精通-SciPy-全-
精通 SciPy(全)
零、前言
编写《精通 SciPy》的想法出现了,但是在发表《数值和科学计算学习》之后两个月。 在南卡罗来纳大学介绍这本书的过程中,我荣幸地向工程师,科学家和学生的不同受众介绍了本书的内容,他们每个人都有非常不同的研究问题和他们自己的一套首选计算资源 。 在演讲之后的几周中,我帮助了一些专业人士过渡到基于 SciPy 的环境。 在这些会议中,我们讨论了 SciPy 在幕后如何与他们已经使用的相同算法集(通常是相同代码)。 我们尝试了一些示例,并系统地获得了可比的性能。 我们立即看到基于健壮的脚本语言的通用环境的明显好处。 通过 SciPy 堆栈,我们发现了一种与同事,学生或雇主进行交流和共享结果的简便方法。 在所有情况下,切换到 SciPy 堆栈都为我们的小组提供了更快的设置,使新手可以快速掌握。
参与此过程的每个人都从新手变为高级用户,最终很快掌握了 SciPy 堆栈。 在大多数情况下,与我一起工作的个人的科学背景使过渡变得无缝。 当他们能够将他们的研究背后的理论与所提供的解决方案进行对比时,掌握过程就变成了现实。 啊哈时刻总是在复制过程中得到仔细的指导和解释的过程中发生。
这恰恰是这本书背后的哲学。 我邀请您参加类似的会议。 每章都被设想为与具有一定科学需求的个人进行对话,这些需求以数值计算表示。 我们在一起发现了相关的例子,这些例子是解决这些问题的不同可能方法,其背后的理论以及每种方法的利弊。
写作过程遵循类似的路径,以获取引人入胜的示例集。 我与几个不同领域的同事进行了交谈。 每个部分都清楚地反映了这些交流。 在编写最具挑战性的章节(最后四个章节)时,这一点至关重要。 为了确保整本书具有相同的质量,并且始终尝试遵循一组严格的标准,需要花更长的时间才能使这些章节满意。 特别要提到的是 NASA Langley 研究中心的 Aaron Dutle,他帮助塑造了计算几何学这一章的一部分; Facebook 的数据分析师 Parsa Bakhtary,激发了本章中关于统计计算在数据分析中的应用的许多技术。 。
这是一次了不起的旅程,有助于加深我对数值方法的理解,拓宽了我在解决问题上的视野,并增强了我的科学成熟度。 我希望它对您有相同的影响。
本书涵盖的内容
第 1 章和“数值线性代数”概述了矩阵在解决科学计算中的作用。 对于理解后续章节的大多数过程和思想而言,这是至关重要的章节。 您将学习如何在 Python 中有效地构造和存储大型矩阵。 然后,我们着手研究对其的基本操作和操作,然后进行因式分解,矩阵方程的解以及特征值/特征向量的计算。
第 2 章,“插值和*似”开发了*似函数的先进技术,并将其应用于科学计算。 这是接下来两章的 segway。
第 3 章,“微分和积分”探索了产生函数导数的不同技术,更重要的是,如何通过积分过程有效地计算面积和体积。 这是两章专门介绍科学计算中数值方法核心的第一章。 第二部分也是第 5 章和“常微分方程的初值问题”的介绍,其中提到了常微分方程。
第 4 章,“非线性方程式和优化”是一门技术性很强的章节,在其中我们讨论根据所涉及函数的种类获得函数系统的根和极值的最佳方法。
第 5 章和“常微分方程的初值问题”是有关实际问题的五个章节中的第一章。 通过示例,我们向您展示解决微分方程组的最流行技术以及一些应用。
第 6 章和“计算几何”浏览了计算机科学这一领域中最重要的算法。
第 7 章和“描述性统计”是关于统计计算及其在数据分析中的应用的两章中的第一章。 在本章中,我们重点介绍概率和数据探索。
第 8 章,“推断和数据分析”是有关数据分析的第二章。 我们专注于统计推断,机器学习和数据挖掘。
第 9 章,“数字图像处理”是本书的最后一章。 在其中,我们探索了图像压缩,编辑,还原和分析的技术。
这本书需要什么
要处理这些示例并尝试使用本书的代码,您所需要的只是带有 SciPy 堆栈的 Python 的最新版本(2.7 或更高版本):NumPy,SciPy 库,matplotlib,IPython,pandas 和 SymPy。 尽管整本书提供了独立安装所有这些程序的方法,但是我们建议您通过科学的 Python 发行版(例如 Anaconda)执行全局安装。
这本书是给谁的
尽管这本书和技术最终旨在供应用数学家,工程师和计算机科学家使用,但此处介绍的材料面向的是更广泛的读者。 所需要做的只是精通 Python,熟悉 iPython,对科学计算中的数值方法有一定的了解,以及对在科学,工程学或数据分析中开发严肃应用程序的浓厚兴趣。
约定
在本书中,您将找到许多可以区分不同类型信息的文本样式。 以下是这些样式的一些示例,并解释了其含义。
文本中的代码字,数据库表名称,文件夹名称,文件名,文件扩展名,路径名,伪 URL,用户输入和 Twitter 句柄如下所示:“我们可以通过使用 include 指令来包含其他上下文。”
任何命令行输入或输出的编写方式如下:
In [7]: %time eigvals, v = spspla.eigsh(A, 5, which='SM')
CPU times: user 19.3 s, sys: 532 ms, total: 19.8 s
Wall time: 16.7 s
In [8]: print eigvals
[ 10.565523 10.663114 10.725135 10.752737 10.774503]
注意
警告或重要提示会出现在这样的框中。
提示
提示和技巧如下所示。
一、数值线性代数
术语“数值线性代数”是指使用矩阵来解决计算科学问题。 在本章中,我们首先学习如何在 Python 中有效地构造这些对象。 我们强调从在线存储库中导入大型稀疏矩阵。 然后,我们继续审查它们的基本操作和操作。 下一步是研究 SciPy 中实现的不同矩阵函数。 我们将继续探索矩阵方程解,特征值及其相应特征向量的计算的不同因式分解。
动机
下图显示了代表一系列网页(从 1 到 8)的图形:

从一个节点到另一个节点的箭头表示存在从发送节点代表的网页到接收节点代表页面的链接。 例如,从节点 2 到节点 1 的箭头表示网页 2 中有指向网页 1 的链接。请注意,网页 4 如何具有两个外部链接(至页面 2 和 8),并且有三个页面链接至网页 4(第 2,6 和 7 页)。 由节点 2,4 和 8 表示的页面乍一看似乎最受欢迎。
有没有一种数学方法可以真正表达网络在网络中的受欢迎程度? Google 的研究人员提出了PageRank的想法,可以通过计算页面链接的数量和质量来大致估计该概念。 它是这样的:
-
我们以以下方式构造此图的过渡矩阵
T={a[i,j]}:如果存在从网页i到网页j的链接,则条目a[i,j]为1/k,并且总数目为 网页i中的外部链接等于k。 否则,该条目仅为零。N个网页的转换矩阵的大小始终为N × N。 在我们的例子中,矩阵的大小为8×8:0 1/2 0 0 0 0 0 0 1 0 1/2 1/2 0 0 0 0 0 0 0 0 0 0 1/3 0 0 1/2 0 0 0 1 1/3 0 0 0 1/2 0 0 0 0 0 0 0 0 0 0 0 0 1/2 0 0 0 0 1/2 0 0 1/2 0 0 0 1/2 1/2 0 1/3 0
让我们打开一个 iPython 会话并将此特定矩阵加载到内存中。
注意
请记住,在 Python 中,索引从零开始,而不是一个。
In [1]: import numpy as np, matplotlib.pyplot as plt, \
...: scipy.linalg as spla, scipy.sparse as spsp, \
...: scipy.sparse.linalg as spspla
In [2]: np.set_printoptions(suppress=True, precision=3)
In [3]: cols = np.array([0,1,1,2,2,3,3,4,4,5,6,6,6,7,7]); \
...: rows = np.array([1,0,3,1,4,1,7,6,7,3,2,3,7,5,6]); \
...: data = np.array([1., 0.5, 0.5, 0.5, 0.5, \
...: 0.5, 0.5, 0.5, 0.5, 1., \
...: 1./3, 1./3, 1./3, 0.5, 0.5])
In [4]: T = np.zeros((8,8)); \
...: T[rows,cols] = data
从过渡矩阵,我们通过在 0 和 1 之间固定一个正常数p并按照公式来创建PageRank矩阵G = (1-p) * T + p * B以获得合适的阻尼系数p。 在此,B是具有与T相同大小的矩阵,其所有条目均等于1/N。 例如,如果我们选择p = 0.15,我们将获得以下PageRank矩阵:
In [5]: G = (1-0.15) * T + 0.15/8; \
...: print G
[[ 0.019 0.444 0.019 0.019 0.019 0.019 0.019 0.019]
[ 0.869 0.019 0.444 0.444 0.019 0.019 0.019 0.019]
[ 0.019 0.019 0.019 0.019 0.019 0.019 0.302 0.019]
[ 0.019 0.444 0.019 0.019 0.019 0.869 0.302 0.019]
[ 0.019 0.019 0.444 0.019 0.019 0.019 0.019 0.019]
[ 0.019 0.019 0.019 0.019 0.019 0.019 0.019 0.444]
[ 0.019 0.019 0.019 0.019 0.444 0.019 0.019 0.444]
[ 0.019 0.019 0.019 0.444 0.444 0.019 0.302 0.019]]
PageRank矩阵具有一些有趣的属性:
- 1 是复数一的特征值。
- 1 实际上是最大的特征值; 所有其他特征值的模量均小于 1。
- 对应于特征值 1 的特征向量具有所有正项。 特别地,对于特征值 1,存在一个唯一的特征向量,其项的总和等于 1。这就是我们所说的
PageRank向量。
scipy.linalg.eig的快速计算为我们找到了特征向量:
In [6]: eigenvalues, eigenvectors = spla.eig(G); \
...: print eigenvalues
[ 1.000+0.j -0.655+0.j -0.333+0.313j -0.333-0.313j –0.171+0.372j -0.171-0.372j 0.544+0.j 0.268+0.j ]
In [7]: PageRank = eigenvectors[:,0]; \
...: PageRank /= sum(PageRank); \
...: print PageRank.real
[ 0.117 0.232 0.048 0.219 0.039 0.086 0.102 0.157]
这些值对应于图表上描述的八个网页中每个网页的PageRank。 如预期的那样,这些值的最大值与第二个网页(0.232)相关联,紧随其后的是第四个网页(0.219),然后是第八个网页(0.157)。 这些值向我们提供了我们正在寻找的信息:第二个网页最受欢迎,其次是第四个,然后是八个。
注意
请注意,此网页网络问题已被转换为数学对象,成为涉及矩阵,特征值和特征向量的等效问题,并已通过线性代数技术得以解决。
转换矩阵稀疏:其大多数条目为零。 在数值线性代数中,具有极大尺寸的稀疏矩阵特别重要,这不仅是因为它们编码具有挑战性的科学问题,而且还因为很难用基本算法来操纵它们。
与其将矩阵中的所有值存储到内存中,不如仅收集非零值,并使用利用这些智能存储方案的算法,这才有意义。 内存管理的好处是显而易见的。 这些方法通常对于此类矩阵更快,并且舍入误差较小,因为通常所涉及的运算要少得多。 这是 SciPy 的另一个优点,因为它包含许多程序来解决以这种方式存储数据的不同问题。 让我们通过另一个示例观察其力量:
佛罗里达大学稀疏矩阵集合是可在线访问的最大矩阵数据库。 截至 2014 年 1 月,它包含 157 组来自各种科学学科的矩阵。 矩阵的大小从非常小的(1×2)到非常大的(2800 万×2800 万)不等。 由于它们出现在不同的工程问题中,因此预计将不断添加更多的矩阵。
提示
有关此数据库的更多信息,请参见数学软件,第 1 卷,。 T.A.,38,Issue 1,2011,pp 1:1-1:25 戴维斯(Davis)和 Y.Hu,或在上在线访问 http://www.cise.ufl.edu/research/sparse/matrices/ 。
例如,数据库中矩阵最多的组是原始的 Harwell-Boeing Collection,具有 292 种不同的稀疏矩阵。 该组也可以在 Matrix Market 上在线访问: http://math.nist.gov/MatrixMarket/ 。
数据库中的每个矩阵都有三种格式:
- Matrix Market Exchange 格式【Boisvert 等 1997】
- Rutherford-Boeing Exchange 格式【Duff 等 1997】
- 专有 Matlab 格式
.mat。
让我们从集合中以矩阵市场交易格式将两个矩阵导入到我们的 iPython 会话中,这两个矩阵打算用于最小二乘问题的解决方案。 这些矩阵位于 www.cise.ufl.edu/research/sparse/matrices/Bydder/mri2.html 。这些数值对应于在 Sonata 1.5-T 扫描仪(Siemens,Erlangen)上获得的幻像数据。 ,德国)使用磁共振图像处理( MRI )设备。 被测物体是用几种金属物体制成的人体头部的模拟。 我们下载相应的 tar 捆绑包并将其解压缩以获取两个 ASCII 文件:
mri2.mtx(最小二乘问题中的主矩阵)mri2_b.mtx(方程式的右侧)
文件mri2.mtx的前 20 行内容如下:

前十六行是注释,为我们提供了有关矩阵生成的一些信息。
- 出现的计算机视觉问题:MRI 重建
- 作者信息:UCSD 的 Mark Bydder
- 应用于数据的过程:解决最小二乘问题
Ax - b,并对结果进行后可视化
第十七行指示矩阵的大小63240行×147456列,以及数据中的非零条目数569160。
文件的其余部分恰好包括 569160 行,每行包含两个整数和一个浮点数:这些是矩阵中非零元素的位置以及相应的值。
提示
我们需要考虑到这些文件使用 FORTRAN 约定,从 1 开始而不是从 0 开始数组。
将文件读入ndarray的一个好方法是通过 NumPy 中的函数loadtxt。 然后,我们可以使用scipy在模块scipy.sparse中使用函数coo_matrix将数组转换为稀疏矩阵(coo 代表坐标内部格式)。
In [8]: rows, cols, data = np.loadtxt("mri2.mtx", skiprows=17, \
...: unpack=True)
In [9]: rows -= 1; cols -= 1;
In [10]: MRI2 = spsp.coo_matrix((data, (rows, cols)), \
....: shape=(63240,147456))
可视化此矩阵稀疏性的最佳方法是借助模块matplotlib.pyplot中的例程spy。
In [11]: plt.spy(MRI2); \
....: plt.show()
我们获得以下图像。 每个像素对应于矩阵中的一个条目; 白色表示零值,非零值根据其大小(越大,越深)用不同的蓝色阴影表示:

这是第二个文件mri2_b.mtx的前十行,它不代表稀疏矩阵,而是列向量:
%% MatrixMarket matrix array complex general
%-------------------------------------------------------------
% UF Sparse Matrix Collection, Tim Davis
% http://www.cise.ufl.edu/research/sparse/matrices/Bydder/mri2
% name: Bydder/mri2 : b matrix
%-------------------------------------------------------------
63240 1
-.07214859127998352 .037707749754190445
-.0729086771607399 .03763720765709877
-.07373382151126862 .03766685724258423
这是六条带信息的带注释的行,另外一行表示向量的形状(63240行和1列),其余各行包含两列浮点值,即浮点值的实部和虚部 相应的数据。 我们继续将这个向量读取到内存中,解决了建议的最小二乘问题,并获得了以下表示模拟人类头部切片的重构:
In [12]: r_vals, i_vals = np.loadtxt("mri2_b.mtx", skiprows=7,
....: unpack=True)
In [13]: %time solution = spspla.lsqr(MRI2, r_vals + 1j*i_vals)
CPU times: user 4min 42s, sys: 1min 48s, total: 6min 30s
Wall time: 6min 30s
In [14]: from scipy.fftpack import fft2, fftshift
In [15]: img = solution[0].reshape(384,384); \
....: img = np.abs(fftshift(fft2(img)))
In [16]: plt.imshow(img); \
....: plt.show()

提示
如果对创建此矩阵背后的理论以及该问题的详细信息感兴趣,请阅读 H.Sedarat 和 D.G.Nishimura 撰写的文章关于网格重构算法的最优性,该文章发表于 IEEE Trans。 医学图像处理,第一卷。 19 号 4,第 306-317 页,2000 年。
对于结构良好的矩阵,这些矩阵将专门涉及矩阵乘法,通常可以用智能方式存储对象。 让我们考虑一个例子。
水*地震振荡会影响高层建筑的各个楼层,具体取决于楼层振荡的固有频率。 如果我们做出某些假设,可以通过竞争获得作为 N 微分方程的二阶系统的模型,该模型可以量化具有 N 层的建筑物的振动:牛顿第二力定律 设定为等于胡克力定律和地震波产生的外力之和。
这些是我们将需要的假设:
- 每个楼层都被视为位于其质量中心的质量点。 楼层有质量
m[1], m[2], ..., m[N]。 - 每个楼层都通过线性恢复力(胡克
-k * elongation)恢复到其*衡位置。 楼层的胡克常数为k[1], k[2], ..., k[N]。 - 表示楼层振动的质量的位置是
x[1], x[2], ..., x[N]。 我们假设它们都是时间函数,并且在*衡时,它们都等于零。 - 为了简化说明,我们将假设没有摩擦:楼层上的所有阻尼效果都将被忽略。
- 楼层的方程式仅取决于相邻楼层。
将质量矩阵M设置为对角矩阵,在其对角线上包含地板质量。 将K(胡克矩阵)设置为具有以下结构的三对角矩阵,对于j每行,除以下各项外,所有条目均为零:
j-1列(我们将其设置为k[j+1])j列(我们设置为-k[j+1]-k[j+1]),以及- 列
j+1,我们将其设置为k[j+2]。
将H设置为包含地震引起的各层外力的列向量,将X设置为包含函数x[j]的列向量。
然后我们有了系统:M * X''= K * X + H。 该系统的齐次部分是M与K的倒数的乘积,我们将其表示为A。
为了解决齐次线性二阶系统X''= A * X,我们定义变量Y包含2 * N项:所有N函数x[j],然后是它们的派生词x'[j]。 该二阶线性系统的任何解都具有一阶线性系统的相应解Y'= C * Y,其中C是大小为2 * N×2 * N的块矩阵 。 此矩阵C由大小为N × N的仅包含零的块组成,其后为单位(大小为N × N),并且在这两个下方,矩阵A在水*方向后跟另一个N×N零块。
不必将此矩阵C存储到内存或其任何因素或块中。 相反,我们将利用其结构,并使用线性运算符来表示它。 然后需要最少的数据来生成该运算符(仅质量值和胡克系数),远小于其任何矩阵表示形式。
让我们展示一个六层楼的具体例子。 我们首先指出它们的质量和胡克常数,然后继续构建A的表示形式为线性算子:
In [17]: m = np.array([56., 56., 56., 54., 54., 53.]); \
....: k = np.array([561., 562., 560., 541., 542., 530.])
In [18]: def Axv(v):
....: global k, m
....: w = v.copy()
....: w[0] = (k[1]*v[1] - (k[0]+k[1])*v[0])/m[0]
....: for j in range(1, len(v)-1):
....: w[j] = k[j]*v[j-1] + k[j+1]*v[j+1] - \
....: (k[j]+k[j+1])*v[j]
....: w[j] /= m[j]
....: w[-1] = k[-1]*(v[-2]-v[-1])/m[-1]
....: return w
....:
In [19]: A = spspla.LinearOperator((6,6), matvec=Axv, matmat=Axv,
....: dtype=np.float64)
现在C的结构非常简单(比其矩阵要简单得多!):
In [20]: def Cxv(v):
....: n = len(v)/2
....: w = v.copy()
....: w[:n] = v[n:]
....: w[n:] = A * v[:n]
....: return w
....:
In [21]: C = spspla.LinearOperator((12,12), matvec=Cxv, matmat=Cxv,
....: dtype=np.float64)
这个齐次系统的解以C的指数作用形式出现:Y(t) = expm(C * t) * Y(0),其中expm()表示矩阵指数函数。 在 SciPy 中,使用模块scipy.sparse.linalg中的例程expm_multiply执行此操作。
例如,在我们的情况下,给定包含值x[1](0)=0, ..., x[N](0)=0, x'[1](0)=1, ..., x'[N](0)=1的初始值,如果我们需要以大小0.1为步长在 0 和 1 之间的t值求解Y(t),则可以发出以下命令:
提示
据报道,在某些安装中,在下一步中,必须给出 C 的矩阵,而不是实际的线性算子(因此与手册相矛盾)。 如果在您的系统中是这种情况,只需将下一行中的 C 更改为其矩阵表示即可。
In [22]: initial_condition = np.zeros(12); \
....: initial_condition[6:] = 1
In [23]: Y = spspla.exp_multiply(C, np.zeros(12), start=0,
....: stop=1, num=10)
然后可以计算出第一层中六层的振动,并绘制出曲线图。 例如,要查看一楼的振荡情况,我们可以发出以下命令:
In [24]: plt.plot(np.linspace(0,1,10), Y[:,0]); \
....: plt.xlabel('time (in seconds)'); \
....: plt.ylabel('oscillation')
我们获得以下图。 请注意,第一层如何在第一十分之一秒内上升,但从原来的高度从 0.1 秒下降到 0.9 秒,几乎下降到一米以下,然后开始缓慢上升:

提示
有关微分方程系统以及如何使用指数函数求解它们的更多详细信息,请阅读例如《 HTHT0]基本微分方程》第 10 版。 ,由 William E. Boyce 和 Richard C. DiPrima 撰写。 威利(Wiley),2012 年。
这三个示例说明了第一章“数值线性代数”的目标。 在 Python 中,这首先要通过以下任何类将数据存储为矩阵形式或作为相关的线性运算符来实现:
numpy.ndarray(确保它们是二维的)numpy.matrixscipy.sparse.bsr_matrix(块稀疏行矩阵)scipy.sparse.coo_matrix(坐标格式的稀疏矩阵)scipy.sparse.csc_matrix(压缩的稀疏列矩阵)scipy.sparse.csr_matrix(压缩的稀疏行矩阵)scipy.sparse.dia_matrix(带有对角存储的稀疏矩阵)scipy.sparse.dok_matrix(基于字典的稀疏矩阵)scipy.sparse.lil_matrix(基于链表的稀疏矩阵)scipy.sparse.linalg.LinearOperator
正如我们在示例中所看到的,不同类的选择主要遵循数据的稀疏性以及我们将应用于它们的算法。
提示
在以下各节中,我们将学习何时应用这些选择。
然后,该选择决定了我们用于不同算法的模块:scipy.linalg用于通用矩阵,而scipy.sparse和scipy.sparse.linalg都用于稀疏矩阵或线性运算符。 这三个 SciPy 模块是在高度优化的计算机库 BLAS(用 Fortran77 编写),LAPACK(在 Fortran90 中编写),ARPACK(在 Fortran77 中)和 SuperLU(在 C 中)的基础上编译的。
注意
为了更好地理解这些基础软件包,请阅读其创建者的描述和文档:
- BLAS: netlib.org/blas/faq.html
- LAPACK: netlib.org/lapack/lapack-3.2.html
- ARPACK: www.caam.rice.edu/software/ARPACK/
- SuperLU: crd-legacy.lbl.gov/~xiaoye/SuperLU/
这三个 SciPy 模块中的大多数例程都是上述库中函数的包装。 如果我们愿意,我们还可以直接调用基础函数。 在scipy.linalg模块中,我们具有以下内容:
scipy.linalg.get_blas_funcs从 BLAS 调用例程scipy.linalg.get_lapack_funcs从 LAPACK 调用例程
例如,如果我们要使用BLAS函数NRM2计算 Frobenius 范数:
In [25]: blas_norm = spla.get_blas_func('nrm2')
In [26]: blas_norm(np.float32([1e20]))
Out[26]: 1.0000000200408773e+20
创建矩阵和线性算子
在本章的第一部分中,我们将专注于有效创建矩阵。 我们首先回顾一些不同的方法来构造基本矩阵作为ndarray实例类,包括对 NumPy 和 SciPy 中已经包含的所有特殊矩阵的枚举。 我们继续研究从基本矩阵构造复杂矩阵的可能性。 我们在matrix实例类中回顾了相同的概念。 接下来,我们详细探讨输入稀疏矩阵的不同方法。 我们以线性算子的构造结束本节。
注意
我们假定您熟悉 NumPy 中的ndarray创建以及数据类型(dtype),索引,两个或多个数组组合的例程,数组操作或从这些对象中提取信息。 在本章中,我们将重点介绍仅对矩阵有意义的函数,方法和例程。 如果它们的输出没有转换为线性代数等价物,我们将不考虑其运算。 有关ndarray的入门知识,建议您浏览《数值科学计算学习》第二版的第二章。 为了快速回顾线性代数,我们推荐 Hoffman 和 Kunze,《线性代数第二版》,Pearson,1971 年。
在 ndarray 类中构造矩阵
我们可以通过三种不同的方式从作为ndarray实例的数据创建矩阵:从标准输入手动创建,通过为每个条目分配函数值或通过从外部文件检索数据来创建矩阵。
建设者
|
描述
|
| --- | --- |
| numpy.array(object) | 从object创建矩阵 |
| numpy.diag(arr, k) | 在对角线k上创建数组arr的条目的对角矩阵 |
| numpy.fromfunction(function, shape) | 通过在每个坐标上执行函数来创建矩阵 |
| numpy.fromfile(fname) | 从文本或二进制文件创建矩阵(基本) |
| numpy.loadtxt(fname) | 从文本文件创建矩阵(高级) |
让我们创建一些示例矩阵来说明上表中定义的一些函数。 和以前一样,我们开始一个 iPython 会话:
In [1]: import numpy as np, matplotlib.pyplot as plt, \
...: scipy.linalg as spla, scipy.sparse as spsp, \
...: scipy.sparse.linalg as spspla
In [2]: A = np.array([[1,2],[4,16]]); \...: A
Out[2]:
array([[ 1, 2],
[ 4, 16]])
In [3]: B = np.fromfunction(lambda i,j: (i-1)*(j+1),
...: (3,2), dtype=int); \
...: print B
...:
[[-1 -2]
[ 0 0]
[ 1 2]]
In [4]: np.diag((1j,4))
Out[4]:
array([[ 0.+1.j, 0.+0.j],
[ 0.+0.j, 4.+0.j]])
具有预定零和一的特殊矩阵可以使用以下函数构造:
|建设者
|
描述
|
| --- | --- |
| numpy.empty(shape) | 给定形状的数组,条目未初始化 |
| numpy.eye(N, M, k) | 二维数组,对角线为k - 1,其他位置为零 |
| numpy.identity(n) | 身份阵列 |
| numpy.ones(shape) | 所有条目等于 1 的数组 |
| numpy.zeros(shape) | 所有条目等于零的数组 |
| numpy.tri(N, M, k) | 在给定对角线处及以下的数组,否则为零 |
提示
除了numpy.tri以外,所有这些构造都具有一个伴随函数xxx_like,该函数创建具有所请求特征且具有与另一个源ndarray类相同的形状和数据类型的ndarray:
In [5]: np.empty_like(A)
Out[5]:
array([[140567774850560, 140567774850560],
[ 4411734640, 562954363882576]])
值得注意的是构造为数值范围的数组。
|建设者
|
描述
|
| --- | --- |
| numpy.arange(stop) | 间隔内的均等值 |
| numpy.linspace(start, stop) | 在一定间隔内均匀间隔的数字 |
| numpy.logspace(start, stop) | 对数刻度上等距的数字 |
| numpy.meshgrid | 来自两个或多个坐标向量的坐标矩阵 |
| numpy.mgrid | nd_grid实例返回密集多维meshgrid |
| numpy.ogrid | nd_grid实例返回开放的多维meshgrid |
可以在 NumPy 和模块scipy.linalg中轻松调用在线性代数中具有众多应用的特殊矩阵。
建设者
|
描述
|
| --- | --- |
| scipy.linalg.circulant(arr) | 一维数组arr生成的循环矩阵 |
| scipy.linalg.companion(arr) | arr编码的多项式的伴随矩阵 |
| scipy.linalg.hadamard(n) | Sylvester 构造大小为n × n的 Hadamard 矩阵。n必须为 2 的幂 |
| scipy.linalg.hankel(arr1, arr2) | 第一列为arr1,最后一列为arr2的汉克矩阵 |
| scipy.linalg.hilbert(n) | 大小为n × n的希尔伯特矩阵 |
| scipy.linalg.invhilbert(n) | n × n大小的希尔伯特矩阵的逆 |
| scipy.linalg.leslie(arr1, arr2) | 具有繁殖力数组arr1和生存系数arr2的莱斯利矩阵 |
| scipy.linalg.pascal(n) | n × n截断了二项式系数的 Pascal 矩阵 |
| scipy.linalg.toeplitz(arr1, arr2) | 第一列arr1和第一行arr2的 Toeplitz 数组 |
| numpy.vander(arr) | 数组arr的范德蒙德矩阵 |
例如,一种获得所有数量级的最大二项式系数(对应的帕斯卡三角形)的快速方法是借助精确的帕斯卡矩阵。 下面的示例显示如何计算这些系数,直到13为止:
In [6]: print spla.pascal(13, kind='lower')

除了这些基本的构造函数之外,我们始终可以以不同的方式堆叠数组:
|建设者
|
描述
|
| --- | --- |
| numpy.concatenate((A1, A2, ...)) | 结合矩阵 |
| numpy.hstack((A1, A2, ...)) | 水*堆叠矩阵 |
| numpy.vstack((A1, A2, ...)) | 垂直堆叠矩阵 |
| numpy.tile(A, reps) | 重复矩阵一定次数(由reps决定) |
| scipy.linalg.block_diag(A1,A2, ...) | 创建块对角线数组 |
让我们观察一下其中的一些构造函数:
In [7]: np.tile(A, (2,3)) # 2 rows, 3 columns
Out[7]:
array([[ 1, 2, 1, 2, 1, 2],
[ 4, 16, 4, 16, 4, 16],
[ 1, 2, 1, 2, 1, 2],
[ 4, 16, 4, 16, 4, 16]])
In [8]: spla.block_diag(A,B)
Out[8]:
array([[ 1, 2, 0, 0],
[ 4, 16, 0, 0],
[ 0, 0, -1, -2],
[ 0, 0, 0, 0],
[ 0, 0, 1, 2]])
在矩阵类中构造矩阵
对于matrix类,直接创建矩阵的通常方法是调用numpy.mat或numpy.matrix。 在创建类似于A的矩阵时,请观察numpy.matrix的语法比numpy.array的语法舒适得多。 使用此语法,用逗号分隔的不同值属于矩阵的同一行。 分号表示行的更改。 还要注意强制转换为matrix类!
In [9]: C = np.matrix('1,2;4,16'); \
...: C
Out[9]:
matrix([[ 1, 2],
[ 4, 16]])
这两个功能还将任何ndarray转换为matrix。 第三个功能可以完成此任务:numpy.asmatrix:
In [10]: np.asmatrix(A)
Out[10]:
matrix([[ 1, 2],
[ 4, 16]])
对于由块组成的矩阵的排列,除了前面描述的ndarray的常见堆栈操作外,我们还具有非常方便的功能numpy.bmat。 请注意与numpy.matrix的语法相似,尤其是使用逗号表示水*串联,使用分号表示垂直串联:
In [11]: np.bmat('A;B') In [12]: np.bmat('A,C;C,A')
Out[11]: Out[12]:
matrix([[ 1, 2], matrix([[ 1, 2, 1, 2],
[ 4, 16], [ 4, 16, 4, 16],
[-1, -2], [ 1, 2, 1, 2],
[ 0, 0], [ 4, 16, 4, 16]])
[ 1, 2]])
构造稀疏矩阵
有七种输入稀疏矩阵的方法。 每种格式旨在使特定问题或操作更有效。 让我们详细了解一下它们:
|方法
|
Name
|
最佳使用
|
| --- | --- | --- |
| BSR | 块稀疏行 | 如果矩阵包含块,则为高效算术。 |
| COO | 坐标 | 快速高效的施工格式。 转换为 CSC 和 CSR 格式的有效方法。 |
| CSC | 压缩稀疏柱 | 高效的矩阵算法和列切片。 矩阵向量乘积相对快。 |
| CSR | 压缩稀疏行 | 高效的矩阵算法和行切片。 执行矩阵向量乘积最快。 |
| DIA | 对角线存储 | 如果矩阵包含非零项的长对角线,则对于构造和存储有效。 |
| DOK | 钥匙字典 | 高效的增量构造和单个矩阵条目的访问。 |
| LIL | 基于行的链接列表 | 切片灵活。 有效更改矩阵稀疏度。 |
最多可以用五种方式填充它们,每种稀疏矩阵格式中共有三种:
-
它们可以强制转换为稀疏任何通用矩阵。
lil格式是使用此方法最有效的方法:In [13]: A_coo = spsp.coo_matrix(A); \ ....: A_lil = spsp.lil_matrix(A) -
它们可以将另一种稀疏格式的另一种稀疏矩阵转换为特定的稀疏格式:
In [14]: A_csr = spsp.csr_matrix(A_coo) -
可以通过指示形状和
dtype来构造任意形状的空稀疏矩阵:In [15]: M_bsr = spsp.bsr_matrix((100,100), dtype=int)
它们都有几种不同的额外输入法,每种输入法都特定于其存储格式。
-
花式索引:就像处理任何通用矩阵一样。 这仅适用于 LIL 或 DOK 格式:
In [16]: M_lil = spsp.lil_matrix((100,100), dtype=int) In [17]: M_lil[25:75, 25:75] = 1 In [18]: M_bsr[25:75, 25:75] = 1 NotImplementedError Traceback (most recent call last) <ipython-input-18-d9fa1001cab8> in <module>() ----> 1 M_bsr[25:75, 25:75] = 1 [...]/scipy/sparse/bsr.pyc in __setitem__(self, key, val) 297 298 def __setitem__(self,key,val): --> 299 raise NotImplementedError 300 301 ###################### NotImplementedError: -
键字典:当我们一次创建,更新或搜索每个元素时,此输入系统最有效。 它仅对 LIL 和 DOK 格式有效:
In [19]: M_dok = spsp.dok_matrix((100,100), dtype=int) In [20]: position = lambda i, j: ((i<j) & ((i+j)%10==0)) In [21]: for i in range(100): ....: for j in range(100): ....: M_dok[i,j] = position(i,j) ....: -
Data, rows, and columns: This is common to four formats: BSR, COO, CSC, and CSR. This is the method of choice to import sparse matrices from the Matrix Market Exchange format, as illustrated at the beginning of the chapter.
提示
使用数据,行和列输入法时,最好在构造中始终包含选项
shape。 如果未提供此值,将从行和列的最大坐标中推断出矩阵的大小,从而可能导致矩阵的大小小于所需的大小。 -
Data, indices, and pointers: This is common to three formats: BSR, CSC, and CSR. It is the method of choice to import sparse matrices from the Rutherford-Boeing Exchange format.
注意
Rutherford-Boeing Exchange 格式是 Harwell-Boeing 格式的更新版本。 它将矩阵存储为三个向量:
pointers_v,indices_v和data。j列的条目的行索引位于向量indices_v的位置pointers_v(j)至pointers_v(j+1)-1中。 矩阵的相应值在矢量数据中位于相同位置。
让我们以示例方式展示如何读取卢瑟福-波音矩阵交换格式Pajek/football中的有趣矩阵。 可以在 www.cise.ufl.edu/research/sparse/matrices/Pajek/football.html 的集合中找到具有 118 个非零条目的 35×35 矩阵。
它是参加 1998 年法国举办的 FIFA 世界杯的所有国家橄榄球队的网络的邻接矩阵。网络中的每个节点代表一个国家(或国家橄榄球队),并且链接显示哪个国家向另一个国家输出了球员 国家。
这是football.rb文件的打印输出:

文件的头(前四行)包含重要信息:
- 第一行为我们提供了矩阵标题
Pajek/football; 1998; L. Krempel; ed: V. Batagelj和用于识别目的的数字键MTRXID=1474。 - 第二行包含四个整数值:
TOTCRD=12(在标头之后包含有效数据的行;请参见[24]),PTRCRD=2(包含指针数据的行数),INDCRD=5(包含索引数据的行数) 和VALCRD=2(包含矩阵非零值的行数)。 请注意,它必须为TOTCRD = PTRCRD + INDCRD + VALCRD。 - 第三行表示矩阵类型
MXTYPE=(iua),在这种情况下,它代表整数矩阵,不对称的压缩列形式。 它还指示行数和列数(NROW=35,NCOL=35),以及非零条目的数量(NNZERO=118)。 对于压缩列格式,不使用最后一个条目,通常将其设置为零。 - 第四列包含以下各列中数据的 Fortran 格式。 指针为
PTRFMT=(20I4),索引为INDFMT=(26I3),非零值为VALFMT=(26I3)。
我们继续打开文件进行读取,将头文件后的每一行存储在 Python 列表中,并从文件的相关行中提取填充矢量indptr,indices和data所需的数据 。 我们首先使用data,indices和pointers方法创建对应的 CSR 格式的稀疏矩阵football:
In [22]: f = open("football.rb", 'r'); \
....: football_list = list(f); \
....: f.close()
In [23]: football_data = np.array([])
In [24]: for line in range(4, 4+12):
....: newdata = np.fromstring(football_list[line], sep=" ")
....: football_data = np.append(football_data, newdata)
....:
In [25]: indptr = football_data[:35+1] - 1; \
....: indices = football_data[35+1:35+1+118] - 1; \
....: data = football_data[35+1+118:]
In [26]: football = spsp.csr_matrix((data, indices, indptr),
....: shape=(35,35))
此时,可以借助名为networkx的 Python 模块将网络及其关联的图形可视化。 我们获得下图,描绘了不同国家的节点。 节点之间的每个箭头表示原始国家/地区已将玩家出口到接收国:
注意
networkx是用于处理复杂网络的 Python 模块。 有关更多信息,请访问其在 networkx.github.io 上的 Github 项目页面。
一种完成此任务的方法如下:
In [27]: import networkx
In [28]: G = networkx.DiGraph(football)
In [29]: f = open("football_nodename.txt"); \
....: m = list(f); \
....: f.close()
In [30]: def rename(x): return m[x]
In [31]: G = networkx.relabel_nodes(G, rename)
In [32]: pos = networkx.spring_layout(G)
In [33]: networkx.draw_networkx(G, pos, alpha=0.2, node_color='w',
....: edge_color='b')

模块scipy.sparse从 NumPy 借鉴了一些有趣的概念来创建构造函数和特殊矩阵:
建设者
|
描述
|
| --- | --- |
| scipy.sparse.diags(diagonals, offsets) | 对角线的稀疏矩阵 |
| scipy.sparse.rand(m, n, density) | 指定密度的随机稀疏矩阵 |
| scipy.sparse.eye(m) | 主对角线中带有 1 的稀疏矩阵 |
| scipy.sparse.identity(n) | n × n大小的身份稀疏矩阵 |
函数diags和rand都应通过示例说明其语法。 我们将从具有两个对角线的大小为 14×14 的稀疏矩阵开始:主对角线包含 1s,下面的对角线包含 2s。 我们还创建了一个具有函数scipy.sparse.rand的随机矩阵。 此矩阵的大小为 5×5,具有 25%的非零元素(density=0.25),并且采用 LIL 格式制作:
In [34]: diagonals = [[1]*14, [2]*13]
In [35]: print spsp.diags(diagonals, [0,-1]).todense()
[[ 1\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0.]
[ 2\. 1\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0.]
[ 0\. 2\. 1\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0.]
[ 0\. 0\. 2\. 1\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0.]
[ 0\. 0\. 0\. 2\. 1\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0.]
[ 0\. 0\. 0\. 0\. 2\. 1\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0.]
[ 0\. 0\. 0\. 0\. 0\. 2\. 1\. 0\. 0\. 0\. 0\. 0\. 0\. 0.]
[ 0\. 0\. 0\. 0\. 0\. 0\. 2\. 1\. 0\. 0\. 0\. 0\. 0\. 0.]
[ 0\. 0\. 0\. 0\. 0\. 0\. 0\. 2\. 1\. 0\. 0\. 0\. 0\. 0.]
[ 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 2\. 1\. 0\. 0\. 0\. 0.]
[ 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 2\. 1\. 0\. 0\. 0.]
[ 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 2\. 1\. 0\. 0.]
[ 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 2\. 1\. 0.]
[ 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 2\. 1.]]
In [36]: S_25_lil = spsp.rand(5, 5, density=0.25, format='lil')
In [37]: S_25_lil
Out[37]:
<5x5 sparse matrix of type '<type 'numpy.float64'>'
with 6 stored elements in LInked List format>
In [38]: print S_25_lil
(0, 0) 0.186663044982
(1, 0) 0.127636181284
(1, 4) 0.918284870518
(3, 2) 0.458768884701
(3, 3) 0.533573291684
(4, 3) 0.908751420065
In [39]: print S_25_lil.todense()
[[ 0.18666304 0\. 0\. 0\. 0\. ]
[ 0.12763618 0\. 0\. 0\. 0.91828487]
[ 0\. 0\. 0\. 0\. 0\. ]
[ 0\. 0\. 0.45876888 0.53357329 0\. ]
[ 0\. 0\. 0\. 0.90875142 0\. ]]
与我们组合ndarray实例的方式类似,我们有一些巧妙的方式来组合稀疏矩阵以构造更复杂的对象:
建设者
|
描述
|
| --- | --- |
| scipy.sparse.bmat(blocks) | 稀疏子块的稀疏矩阵 |
| scipy.sparse.hstack(blocks) | 水*堆叠稀疏矩阵 |
| scipy.sparse.vstack(blocks) | 垂直堆叠稀疏矩阵 |
线性算子
线性运算符基本上是一个函数,该函数通过将输入与矩阵进行左乘法运算,将一列向量作为输入并输出另一个列向量。 尽管从技术上讲,我们可以仅通过处理相应的矩阵来表示这些对象,但还有更好的方法可以做到这一点。
|建设者
|
描述
|
| --- | --- |
| scipy.sparse.linalg.LinearOperator(shape, matvec) | 执行矩阵向量乘积的通用接口 |
| scipy.sparse.linalg.aslinearoperator(A) | 将A返回为LinearOperator |
在scipy.sparse.linalg模块中,我们有一个处理这些对象的公共接口:LinearOperator类。 此类仅具有以下两个属性和三个方法:
shape:表示矩阵的形状dtype:矩阵的数据类型matvec:执行矩阵与向量的乘法rmatvec:通过矩阵与向量的共轭转置来进行乘法matmat:执行一个矩阵与另一个矩阵的乘法
通过示例可以最好地说明其用法。 考虑两个函数,它们分别使用大小为 3 的向量和大小为 4 的输出向量,并分别与两个各自大小为 4×3 的矩阵相乘。我们可以很好地用 lambda 谓词定义这些函数:
In [40]: H1 = np.matrix("1,3,5; 2,4,6; 6,4,2; 5,3,1"); \
....: H2 = np.matrix("1,2,3; 1,3,2; 2,1,3; 2,3,1")
In [41]: L1 = lambda x: H1.dot(x); \
....: L2 = lambda x: H2.dot(x)
In [42]: print L1(np.ones(3))
[[ 9\. 12\. 12\. 9.]]
In [43]: print L2(np.tri(3,3))
[[ 6\. 5\. 3.]
[ 6\. 5\. 2.]
[ 6\. 4\. 3.]
[ 6\. 4\. 1.]]
现在,当我们尝试对这两个函数进行加/减或将它们中的任何一个乘以标量时,就会出现一个问题。 从技术上讲,它应该像加/减相应的矩阵,或将它们乘以任意数字,然后再次执行所需的左乘法一样简单。 但事实并非如此。
例如,我们想写(L1+L2)(v)而不是L1(v) + L2(v)。 不幸的是,这样做会引发错误:
TypeError: unsupported operand type(s) for +: 'function' and
'function'
相反,我们可以实例化相应的线性运算符并按如下方式随意对其进行操作:
In [44]: Lo1 = spspla.aslinearoperator(H1); \
....: Lo2 = spspla.aslinearoperator(H2)
In [45]: Lo1 - 6 * Lo2
Out[45]: <4x3 _SumLinearOperator with dtype=float64>
In [46]: print Lo1 * np.ones(3)
[ 9\. 12\. 12\. 9.]
In [47]: print (Lo1-6*Lo2) * np.tri(3,3)
[[-27\. -22\. -13.]
[-24\. -20\. -6.]
[-24\. -18\. -16.]
[-27\. -20\. -5.]]
当描述具有相关矩阵的产品所需的信息量少于存储矩阵的非零元素所需的内存量时,线性运算符是一个很大的优势。
例如,置换矩阵是*方二进制矩阵(一和零),在每一行和每一列中只有一个条目。 考虑一个由大小为 512×512 的四个块组成的大排列矩阵,例如 1024×1024:一个零块,水*跟随一个标识块,在一个标识块的顶部,水*跟随一个另一个零块。 我们可以通过三种不同的方式存储此矩阵:
In [47]: P_sparse = spsp.diags([[1]*512, [1]*512], [512,-512], \
....: dtype=int)
In [48]: P_dense = P_sparse.todense()
In [49]: mv = lambda v: np.roll(v, len(v)/2)
In [50]: P_lo = spspla.LinearOperator((1024,1024), matvec=mv, \
....: matmat=mv, dtype=int)
在稀疏情况下P_sparse,我们可以将其视为仅存储 1024 个整数。 在密集情况下P_dense,我们在技术上存储 1048576 整数值。 对于线性算子,实际上看起来我们没有存储任何东西! 指示如何执行乘法的函数mv的占用空间比任何相关矩阵小得多。 这也反映在执行与这些对象的乘法的时间上:
In [51]: %timeit P_sparse * np.ones(1024)
10000 loops, best of 3: 29.7 µs per loop
In [52]: %timeit P_dense.dot(np.ones(1024))
100 loops, best of 3: 6.07 ms per loop
In [53]: %timeit P_lo * np.ones(1024)
10000 loops, best of 3: 25.4 µs per loop
基本矩阵处理
本章第二部分的重点是掌握以下操作:
- 标量乘法,矩阵加法和矩阵乘法
- 痕迹和决定因素
- 转置和反转
- 规范和条件编号
标量乘法,矩阵加法和矩阵乘法
让我们从ndarray类存储的矩阵开始。 我们使用*运算符完成标量乘法,并使用+运算符完成矩阵加法。 但是对于矩阵乘法,我们将需要实例方法dot()或numpy.dot函数,因为运算符*保留用于逐元素乘法:
In [54]: 2*A
Out[54]:
array([[ 2, 4],
[ 8, 32]])
In [55]: A + 2*A
Out[55]:
array([[ 3, 6],
[12, 48]])
In [56]: A.dot(2*A) In [56]: np.dot(A, 2*A)
Out[56]: Out[56]:
array([[ 18, 68], array([[ 18, 68],
[136, 528]]) [136, 528]])
In [57]: A.dot(B)
ValueError: objects are not aligned
In [58]: B.dot(A) In [58]: np.dot(B, A)
Out[58]: Out[58]:
array([[ -9, -34], array([[ -9, -34],
[ 0, 0], [ 0, 0],
[ 9, 34]]) [ 9, 34]])
矩阵类使矩阵乘法更加直观:可以使用运算符*代替dot()方法。 还要注意不同实例类ndarray与矩阵之间的矩阵乘法如何始终强制转换为matrix实例类:
In [59]: C * B
ValueError: shapes (2,2) and (3,2) not aligned: 2 (dim 1) != 3 (dim 0)
In [60]: B * C
Out[60]:
matrix([[ -9, -34],
[ 0, 0],
[ 9, 34]])
对于稀疏矩阵,即使两个稀疏类都不相同,标量乘法和加法也可以与明显的运算符一起很好地工作。 注意每个操作后的结果类转换:
In [61]: S_10_coo = spsp.rand(5, 5, density=0.1, format='coo')
In [62]: S_25_lil + S_10_coo
Out[62]: <5x5 sparse matrix of type '<type 'numpy.float64'>'
with 8 stored elements in Compressed Sparse Row format>
In [63]: S_25_lil * S_10_coo
Out[63]: <5x5 sparse matrix of type '<type 'numpy.float64'>'
with 4 stored elements in Compressed Sparse Row format>
提示
numpy.dot不适用于稀疏矩阵与泛型的矩阵乘法。 我们必须改用运算符*。
In [64]: S_100_coo = spsp.rand(2, 2, density=1, format='coo')
In [65]: np.dot(A, S_100_coo)
Out[66]:
array([[ <2x2 sparse matrix of type '<type 'numpy.float64'>'
with 4 stored elements in COOrdinate format>,
<2x2 sparse matrix of type '<type 'numpy.float64'>'
with 4 stored elements in COOrdinate format>],
[ <2x2 sparse matrix of type '<type 'numpy.float64'>'
with 4 stored elements in COOrdinate format>,
<2x2 sparse matrix of type '<type 'numpy.float64'>'
with 4 stored elements in COOrdinate format>]], dtype=object)
In [67]: A * S_100_coo
Out[68]:
array([[ 1.81 , 1.555],
[ 11.438, 11.105]])
痕迹和决定因素
矩阵的迹线是对角线上元素的总和(假设两个维度上的索引总是增加)。 对于通用矩阵,我们使用实例方法trace()或函数numpy.trace进行计算:
In [69]: A.trace() In [71]: C.trace()
Out[69]: 17 Out[71]: matrix([[17]])
In [70]: B.trace() In [72]: np.trace(B, offset=-1)
Out[70]: -1 Out[72]: 2
为了计算通用*方矩阵的行列式,我们需要在模块scipy.linalg中使用函数det:
In [73]: spla.det(C)
Out[73]: 8.0
转置和反转
可以使用两种实例方法transpose()或T中的任何一种来计算转置,这是两类通用矩阵中的任何一种:
In [74]: B.transpose() In [75]: C.T
Out[74]: Out[75]:
array([[-1, 0, 1], matrix([[ 1, 4],
[-2, 0, 2]]) [ 2, 16]])
可以使用实例方法H为matrix类计算厄米转置:
In [76]: D = C * np.diag((1j,4)); print D In [77]: print D.H
[[ 0.+1.j 8.+0.j] [[ 0.-1.j 0.-4.j]
[ 0.+4.j 64.+0.j]] [ 8.-0.j 64.-0.j]]
使用模块scipy.linalg中的函数inv为ndarray类计算非奇异*方矩阵的逆。 对于matrix类,我们也可以使用实例方法I。 对于非奇异方形稀疏矩阵,我们可以在模块scipy.sparse.linalg中使用函数inv。
提示
稀疏矩阵的逆很少是稀疏的。 因此,不建议通过scipy.sparse.inv功能执行此操作。 解决此问题的一种可能方法是使用todense()实例方法将矩阵转换为通用矩阵,而改用scipy.linear.inv。
但是,由于难以对大矩阵求逆,因此通常最好对逆矩阵进行*似计算。 模块scipy.sparse.linalg中的函数spilu为我们提供了一种非常快速的算法,以CSC格式对*方稀疏矩阵执行此计算。 该算法基于 LU 分解,并在内部进行编码,作为库SuperLU中函数的包装。 它的使用相当复杂,我们将推迟其研究,直到我们探讨matrix分解。
In [78]: E = spsp.rand(512, 512, density=1).todense()
In [79]: S_100_csc = spsp.rand(512, 512, density=1, format='csc')
In [80]: %timeit E.I
10 loops, best of 3: 28.7 ms per loop
In [81]: %timeit spspla.inv(S_100_csc)
1 loops, best of 3: 1.99 s per loop
注意
在执行稀疏逆时,如果输入矩阵不是 CSC 或 CSR 格式,则会收到警告:
/scipy/sparse/linalg/dsolve/linsolve.py:88: SparseEfficiencyWarning: spsolve requires A be CSC or CSR matrix format
warn('spsolve requires A be CSC or CSR matrix format', SparseEfficiencyWarning)
/scipy/sparse/linalg/dsolve/linsolve.py:103: SparseEfficiencyWarning: solve requires b be CSC or CSR matrix format
可以使用模块scipy.linalg中的pinv或pinv2例程,为任何类型的矩阵(不一定为正方形)计算 Moore-Penrose 伪逆。 第一种方法pinv求助于解决最小二乘问题以计算伪逆。 函数pinv2通过基于奇异值分解的方法来计算伪逆。 对于 Hermitian 矩阵或没有复数系数对称的矩阵,我们还有一个名为pinvh的第三函数,它基于特征值分解。
已知在正方形非奇异矩阵的情况下,逆和伪逆是相同的。 这个简单的例子显示了使用以下五种方法计算大型通用对称矩阵的逆的时间:
In [82]: F = E + E.T # F is symmetric
In [83]: %timeit F.I
1 loops, best of 3: 24 ms per loop
In [84]: %timeit spla.inv(F)
10 loops, best of 3: 28 ms per loop
In [85]: %timeit spla.pinvh(E)
1 loops, best of 3: 120 ms per loop
In [86]: %timeit spla.pinv2(E)
1 loops, best of 3: 252 ms per loop
In [87]: %timeit spla.pinv(F)
1 loops, best of 3: 2.21 s per loop
规范和条件编号
对于通用矩阵,我们在scipy.linalg中有七个不同的标准规范。 我们可以在下表中总结它们:
建设者
|
描述
|
| --- | --- |
| norm(A,numpy.inf) | 每行中条目绝对值的总和。 选择最大的价值。 |
| norm(A,-numpy.inf) | 每行中条目绝对值的总和。 选择最小值。 |
| norm(A,1) | 每列中条目的绝对值总和。 选择最大的价值。 |
| norm(A,-1) | 每列中条目的绝对值总和。 选择最小值。 |
| norm(A,2) | 矩阵的最大特征值。 |
| norm(A,-2) | 矩阵的最小特征值。 |
| norm(A,'fro')或norm(A,'f') | Frobenius 范数:乘积A.H * A的迹线的*方根。 |
In [88]: [spla.norm(A,s) for s in (np.inf,-np.inf,-1,1,-2,2,'fro')]
Out[88]: [20, 3, 5, 18, 0.48087417361008861, 16.636368595013604, 16.643316977093239]
提示
对于稀疏矩阵,我们始终可以通过在计算之前应用todense()实例方法来计算范数。 但是,当矩阵的大小太大时,这是不切实际的。 在这些情况下,由于模块scipy.sparse.linalg中的函数onenormest,我们对于 1-范数可以获得的最佳下界是:
In [89]: spla.norm(S_100_csc.todense(), 1) - \
....: spspla.onenormest(S_100_csc)
Out[89]: 0.0
对于 2 范数,我们可能会找到最小和最大特征值的值,但仅适用于*方矩阵。 我们在模块scipy.sparse.linalg中有两种算法可以执行此任务:eigs(用于通用*方矩阵)和 eigsh(用于实对称矩阵)。 在下一节中讨论矩阵分解和因式分解时,我们将详细探讨它们。
注意 SciPy 和 NumPy 的范数计算之间的细微差异。 例如,在 Frobenius 范数的情况下,scipy.linalg.norm直接基于称为 NRM2 的 BLAS 函数,而numpy.linalg.norm等效于形式为sqrt(add.reduce((x.conj() * x).real))的纯粹直接的计算。 当单精度算术中某些数据太大或太小时,基于 BLAS 的代码的优点是速度更快,而且很明显。 在下面的示例中显示:
In [89]: a = np.float64([1e20]); \
....: b = np.float32([1e20])
In [90]: [np.linalg.norm(a), spla.norm(a)]
Out[90]: [1e+20, 1e+20]
In [91]: np.linalg.norm(b)
[...]/numpy/linalg/linalg.py:2056: RuntimeWarning: overflow encountered in multiply
return sqrt(add.reduce((x.conj() * x).real, axis=None))
Out[91]: inf
In [92]: spla.norm(b)
Out[92]: 1.0000000200408773e+20
这不可避免地使我们对非奇异方阵A的条件数的计算进行了讨论。 当我们对输入自变量b进行较小的更改时,此值测量线性方程A * x = b的解的输出将更改多少。 如果该值接*于 1,我们可以放心,解决方案的变化将很小(我们说系统状况良好)。 如果条件数很大,我们就知道系统的计算解可能有问题(然后我们说它是病态的)。
通过将A的范数与其反范数相乘来执行此条件数的计算。 请注意,有不同的条件编号,具体取决于我们为计算选择的标准。 尽管我们需要意识到其明显的局限性,但也可以使用函数numpy.linalg.cond为每个预定义的规范计算这些值。
In [93]: np.linalg.cond(C, -np.inf)
Out[93]: 1.875
矩阵函数
矩阵函数是通过幂级数将*方矩阵映射到另一个*方矩阵的函数。 这些不应与向量化混淆:将一个变量的任何给定函数应用于矩阵的每个元素。 例如,计算方阵A.dot(A)(例如In [8])的*方与将A的所有元素都*方的矩阵不同(示例In [5]至In [] )。
注意
为了在符号上进行适当的区分,我们将写A^2表示方阵的实际*方,写A^n表示随后的幂(对于所有正整数n)。
建设者
|
描述
|
| --- | --- |
| scipy.linalg.funm(A, func, disp) | 将名为func的标量值函数扩展到矩阵 |
| scipy.linalg.fractional_matrix_power(A, t) | 小数矩阵幂 |
| scipy.linalg.expm(A) or scipy.sparse.linalg.expm(A) | 矩阵指数 |
| scipy.sparse.linalg.expm_multiply(A,B) | A的矩阵指数对B的作用 |
| scipy.linalg.expm_frechet(A, E) | E方向上矩阵指数的 Frechet 导数 |
| scipy.linalg.cosm(A) | 矩阵余弦 |
| scipy.linalg.sinm(A) | 矩阵 |
| scipy.linalg.tanm(A) | 矩阵切线 |
| scipy.linalg.coshm(A) | 双曲矩阵余弦 |
| scipy.linalg.sinhm(A) | 双曲矩阵正弦 |
| scipy.linalg.tanhm(A) | 双曲矩阵切线 |
| scipy.linalg.signm(A) | 矩阵符号功能 |
| scipy.linalg.sqrtm(A, disp, blocksize) | 矩阵*方根 |
| scipy.linalg.logm(A, disp) | 矩阵对数 |
In [1]: import numpy as np, scipy as sp; \
...: import scipy.linalg as spla
In [2]: np.set_printoptions(suppress=True, precision=3)
In [3]: def square(x): return x**2
In [4]: A = spla.hilbert(4); print A
[[ 1\. 0.5 0.333 0.25 ]
[ 0.5 0.333 0.25 0.2 ]
[ 0.333 0.25 0.2 0.167]
[ 0.25 0.2 0.167 0.143]]
In [5]: print square(A)
[[ 1\. 0.25 0.111 0.062]
[ 0.5 0.333 0.25 0.2 ]
[ 0.333 0.25 0.2 0.167]
[ 0.25 0.2 0.167 0.143]]
In [6]: print A*A
[[ 1\. 0.25 0.111 0.062]
[ 0.25 0.111 0.062 0.04 ]
[ 0.111 0.062 0.04 0.028]
[ 0.062 0.04 0.028 0.02 ]]
In [7]: print A**2
[[ 1\. 0.25 0.111 0.062]
[ 0.25 0.111 0.062 0.04 ]
[ 0.111 0.062 0.04 0.028]
[ 0.062 0.04 0.028 0.02 ]]
In [8]: print A.dot(A)
[[ 1.424 0.8 0.567 0.441]
[ 0.8 0.464 0.333 0.262]
[ 0.567 0.333 0.241 0.19 ]
[ 0.441 0.262 0.19 0.151]]
矩阵的实际功率A^n是定义任何矩阵函数的起点。 在模块numpy.linalg中,我们具有例程matrix_power来执行此操作。 我们还可以使用通用功能funm或功能fractional_matrix_power来实现此结果,它们都在模块scipy.linalg中。
In [9]: print np.linalg.matrix_power(A, 2)
[[ 1.424 0.8 0.567 0.441]
[ 0.8 0.464 0.333 0.262]
[ 0.567 0.333 0.241 0.19 ]
[ 0.441 0.262 0.19 0.151]]
In [10]: print spla.fractional_matrix_power(A, 2)
[[ 1.424 0.8 0.567 0.441]
[ 0.8 0.464 0.333 0.262]
[ 0.567 0.333 0.241 0.19 ]
[ 0.441 0.262 0.19 0.151]]
In [11]: print spla.funm(A, square)
[[ 1.424 0.8 0.567 0.441]
[ 0.8 0.464 0.333 0.262]
[ 0.567 0.333 0.241 0.19 ]
[ 0.441 0.262 0.19 0.151]]
为了计算任何矩阵函数,理论上,我们首先通过泰勒展开将函数表示为幂级数。 然后,我们将输入矩阵应用于该扩展的*似值(因为不可能无限地添加矩阵)。 因此,大多数矩阵函数必然会带来计算错误。 在scipy.linalg模块中,矩阵功能按照该原理编码。
- 注意,三个函数带有可选的布尔参数
disp。 要了解此参数的用法,我们必须记住,大多数矩阵函数都计算*似值,但存在计算误差。 默认情况下,参数disp设置为True,如果*似误差较大,则会发出警告。 如果将disp设置为False,而不是警告,我们将获得估计误差的 1-范数。 - 函数
expm,矩阵上的指数作用expm_multiply和指数expm_frechet的 Frechet 导数背后的算法使用 Pade *似代替泰勒展开。 这允许更健壮和准确的计算。 所有三角函数和双曲线三角函数均基于涉及expm的简单计算建立其算法。 - 称为
funm的通用矩阵函数和称为sqrtm的*方根函数应用了巧妙的算法,可与输入矩阵的 Schur 分解配合使用,并具有相应的特征值进行适当的代数运算。 它们仍然容易出现舍入错误,但是比任何基于泰勒展开的算法都更快,更准确。 - 称为
signm的矩阵符号函数最初是具有适当功能的funm的应用程序,但是如果该方法失败,则该算法将基于收敛到该解的*似值的迭代采用另一种方法。 - 函数
logm和fractional_matrix_power(将后者应用于非整数幂时)使用 Pade *似和 Schur 分解的非常复杂的组合(并且进行了改进!)。
提示
当我们处理与特征值有关的矩阵分解时,我们将探讨 Schur 分解。 同时,如果您有兴趣学习这些聪明算法的细节,请阅读 Golub 和 Van Loan 的描述,《矩阵计算 4 版》,约翰霍普金斯大学数学科学学院,第一卷。 3。
有关 Schur-Pade 算法以及指数的 Frechet 导数背后的算法的改进的详细信息,请参阅:
- Nicholas J. Higham 和 Lijing Lin 的一种改进的 Schur-Pade 算法,用于求解矩阵及其分数函数的分数幂
- Awad H. Al-Mohy 和 Nicholas J. Higham 用于矩阵对数的改进的逆缩放和*方算法,《SIAM 科学计算杂志》,34(4)
与求解矩阵方程有关的矩阵分解
矩阵分解的概念使数值线性代数成为科学计算中的有效工具。 如果表示问题的矩阵足够简单,则任何基本的通用算法都可以最佳地找到解决方案(即,快速,最少的数据存储并且没有明显的舍入误差)。 但是,在现实生活中,这种情况很少发生。 在一般情况下,我们要做的是找到合适的矩阵分解并定制在每个因子上均最佳的算法,从而在每个步骤上都获得明显的优势。 在本节中,我们将探讨模块scipy.linalg和scipy.sparse.linalg中包含的不同分解因子,这些因子有助于我们实现矩阵方程的稳健解。
相关因式分解
在此类别中,我们具有以下分解式:
透视 LU 分解
始终可以对*方矩阵A进行乘积分解A = P ● L ● U作为置换矩阵P (执行A行的置换),下三角矩阵L和上三角矩阵U:
建设者
|
描述
|
| --- | --- |
| scipy.linalg.lu(A) | 透视 LU 分解 |
| scipy.linalg.lu_factor(A) | 透视 LU 分解 |
| scipy.sparse.linalg.splu(A) | 透视 LU 分解 |
| scipy.sparse.linalg.spilu(A) | 枢轴 LU 分解不完全 |
胆固醇分解
对于正方形,对称和正定矩阵A,我们可以将矩阵实现为上三角三角形的乘积A = U^T ● U矩阵U与其转置的矩阵,或者作为下三角矩阵L与其转置的乘积A = L^T ● L。 U或L的所有对角线条目均严格为正数:
建设者
|
描述
|
| --- | --- |
| scipy.linalg.cholesky(A) | 胆固醇分解 |
| scipy.linalg.cholesky_banded(AB) | Hermitian 正定带状矩阵的 Cholesky 分解 |
QR 分解
我们可以将大小为 m×n 的任何矩阵实现为大小为 m×m 的*方正交矩阵Q的乘积A = Q ● R与上三角矩阵[ 与A大小相同的R。
建设者
|
描述
|
| --- | --- |
| scipy.linalg.qr(A) | 矩阵的 QR 分解 |
奇异值分解
我们可以实现任何矩阵A作为乘积A = U ● D ● V^H带有对角矩阵D的 unit 矩阵U(对角线中的所有条目均为正数),以及另一个 unit 矩阵V的 Hermitian 转置。 D的对角线上的值称为A的奇异值。
建设者
|
描述
|
| --- | --- |
| scipy.linalg.svd(A) | 奇异值分解 |
| scipy.linalg.svdvals(A) | 奇异值 |
| scipy.linalg.diagsvd(s, m, n) | SVD 的对角矩阵,由奇异值 s 和指定大小 |
| scipy.sparse.linalg.svds(A) | 稀疏矩阵的最大k奇异值/向量 |
矩阵方程
在 SciPy 中,我们具有基于以下情况的强大算法来求解任何矩阵方程:
- 给定一个正方形矩阵
A和一个右侧b(它可以是一维矢量或与A相同行数的另一个矩阵),基本系统如下:A ● x = bA^T ● x = bA^H ● x = b
- 给定任何矩阵
A(不一定是正方形)和适当大小的右侧矢量/矩阵b,方程A ● x = b的最小二乘解。 也就是说,找到了使表达A ● x - b的 Frobenius 范数最小的向量x。 - 对于与之前相同的情况,以及一个额外的阻尼系数
d,方程A ● x = b的正则化最小二乘解使函数norm(A * x - b, 'f')**2 + d**2 * norm(x, 'f')**2最小。 - 给定方阵
A和B,以及具有适当大小的右侧矩阵Q,Sylvester 系统为A ● X + X ● B = Q。 - 对于适当大小的方阵
A和矩阵Q,连续的 Lyapunov 方程为A ● X + X ● A^H = Q。 - 对于矩阵
A和Q,与之前的情况一样,离散 Lyapunov 方程为X - A ● X ● A^H = Q。 - 给定*方矩阵
A,Q和R,以及具有适当大小的另一个矩阵B,连续代数 Riccati 方程为A^T ● X + X ● A - X ● B ● R^(-1) ● B^T ● X + Q = 0。 - 对于与前述情况相同的矩阵,离散代数 Riccati 方程为
X = A^T ● X ● A - (A^T ● X ● B) ● (R + B^T ● X ● B)^(-1) ● (B^T ● X ● A) + Q。
无论如何,用 SciPy 掌握矩阵方程式基本上意味着确定所涉及的矩阵并在库中选择最合适的算法来执行所请求的运算。 除了能够以最小的舍入误差量来计算解决方案外,我们还需要以尽可能最快的方式并使用尽可能少的内存资源来进行计算。
向前和向后替代
让我们从最简单的情况开始:线性方程组的基本系统A ● x = b(或其他两个变体),其中A是通用的下限或上限 三角方阵。 从理论上讲,这些系统可以通过正向替换(对于较低的三角矩阵)或向后替换(对于较高的三角矩阵)轻松解决。 在 SciPy 中,我们使用模块scipy.linalg中的功能solve_triangular完成此任务。
在此初始示例中,我们将A构造为大小为 1024×1024 的下三角 Pascal 矩阵,其中已过滤了非零值:奇数变为 1,而偶数变为 0。 右侧b是带有1024的矢量。
In [1]: import numpy as np, \
...: scipy.linalg as spla, scipy.sparse as spsp, \
...: scipy.sparse.linalg as spspla
In [2]: A = (spla.pascal(1024, kind='lower')%2 != 0)
In [3]: %timeit spla.solve_triangular(A, np.ones(1024))
10 loops, best of 3: 6.64 ms per loop
为了解决涉及矩阵A的其他相关系统,我们采用了可选参数trans(默认设置为0或N,为基本系统A ● x = b)。 如果将trans设置为T或1,我们将求解系统A^T ● x = b。 如果将trans设置为C或2,则我们求解A^H ● x = b。
注意
函数solve_triangular是LAPACK函数trtrs的包装器。
基本系统:带状矩阵
就算法简单性而言,接下来的情况是基本系统A ● x = b,其中A是方带矩阵。 我们使用例程solve_banded(用于通用带状矩阵)或solveh_banded(用于复杂的 Hermitian 带状矩阵的通用实对称)。 它们都属于模块scipy.linalg。
注意
函数solve_banded和solveh_banded分别是LAPACK函数GBSV和PBSV的包装。
这两个函数都不接受通常格式的矩阵。 例如,由于solveh_banded需要一个对称的带状矩阵,因此该函数仅需要输入从上到下依次存储的主对角线上,下和上的对角线元素。
通过一个具体的例子可以最好地解释这种输入方法。 取以下对称带状矩阵:
2 -1 0 0 0 0
-1 2 -1 0 0 0
0 -1 2 -1 0 0
0 0 -1 2 -1 0
0 0 0 -1 2 -1
0 0 0 0 -1 2
矩阵的大小为 6×6,只有三个非零对角线,由于对称性,其中两个是相同的。 我们通过以下两种方式之一收集大小为 2×6 的ndarray中两个相关的非零对角线:
-
如果我们决定从上三角矩阵输入条目,则首先收集从上到下的对角线(在主对角线结束),右对齐:
* -1 -1 -1 -1 -1 2 2 2 2 2 2 -
如果我们决定从较低的三角矩阵输入条目,则从顶部到底部(从主对角线开始)收集对角线,左对齐:
2 2 2 2 2 2 -1 -1 -1 -1 -1 * In [4]: B_banded = np.zeros((2,6)); \ ...: B_banded[0,1:] = -1; \ ...: B_banded[1,:] = 2 In [5]: spla.solveh_banded(B_banded, np.ones(6)) Out[5]: array([ 3., 5., 6., 6., 5., 3.])
对于非对称带状方矩阵,我们改用solve_banded,并且输入矩阵也需要以这种特殊方式存储:
- 计算主对角线下的非零对角线数(将其设置为
l)。 计算主对角线上非零对角线的数量(将其设置为u)。 设置r = l + u + 1。 - 如果矩阵的大小为
n × n,则用n列和r行创建ndarray。 我们简称为AB形式的矩阵或AB矩阵。 - 从上到下,仅按顺序将相关的非零对角线存储在 AB 矩阵中。 主对角线上的对角线是右对齐的; 主对角线下方的对角线保持左对齐。
让我们用另一个例子来说明这个过程。 我们输入以下矩阵:
2 -1 0 0 0 0
-1 2 -1 0 0 0
3 -1 2 -1 0 0
0 3 -1 2 -1 0
0 0 3 -1 2 -1
0 0 0 3 -1 2
In [6]: C_banded = np.zeros((4,6)); \
...: C_banded[0,1:] = -1; \
...: C_banded[1,:] = 2; \
...: C_banded[2,:-1] = -1; \
...: C_banded[3,:-2] = 3; \
...: print C_banded
[[ 0\. -1\. -1\. -1\. -1\. -1.]
[ 2\. 2\. 2\. 2\. 2\. 2.]
[-1\. -1\. -1\. -1\. -1\. 0.]
[ 3\. 3\. 3\. 3\. 0\. 0.]]
要调用求解器,我们需要手动输入对角线上方和下方的对角线数量,以及AB矩阵和系统的右侧:
In [7]: spla.solve_banded((2,1), C_banded, np.ones(6))
Out[7]:
array([ 0.86842105, 0.73684211, -0.39473684, 0.07894737,
1.76315789, 1.26315789])
让我们检查一下可以包含在这两个函数的调用中的可选参数:
|参数
|
默认值
|
描述
|
| --- | --- | --- |
| l_and_u | (int, int) | 非零上下对角线数 |
| ab | AB格式的矩阵 | 带状方矩阵 |
| b | ndarray | 右侧 |
| overwrite_ab | 布尔型 | 舍弃ab中的数据 |
| overwrite_b | 布尔型 | 舍弃b中的数据 |
| check_finite | 布尔型 | 是否检查输入矩阵是否包含有限数 |
提示
scipy.linalg模块中需要矩阵作为输入和输出的所有函数,或者方程组的解或因式分解的函数,都有两个我们需要熟悉的可选参数:overwrite_x(对于矩阵中的每个矩阵/矢量 输入)和check_finite。 它们都是布尔值。
默认情况下,overwrite选项设置为False。 如果我们不关心保留输入矩阵的值,则可以使用内存中的同一对象执行操作,而不是在内存中创建另一个具有相同大小的对象。 在这种情况下,我们可以提高速度并减少资源消耗。
默认情况下,check_finite选项设置为True。 在存在数据的算法中,存在可选的数据完整性检查。 如果在任何给定时刻,任何值为(+/-)numpy.inf或NaN,则过程将暂停,并引发异常。 我们可能会关闭此选项,从而导致更快的解决方案,但是如果数据在计算中的任何时候被破坏,代码可能会崩溃。
函数solveh_banded具有一个额外的可选布尔参数lower,该参数最初设置为False。 如果设置为True,则必须输入目标 AB 矩阵的下三角矩阵,而不是上三角矩阵(输入约定与以前相同)。
基本系统:通用*方矩阵
对于A是通用方矩阵的基本系统的解决方案,将A分解为一个好主意,以使某些(或全部)因子为三角形,然后在适当时应用来回替换。 这是枢轴LU和 Cholesky 分解背后的想法。
如果矩阵A是实对称(或复 Hermitian)并且是正定的,则最佳策略是通过应用两个可能的 Cholesky 分解中的任何一个A = U^H ● U或A = L ● L^H,带有U和L上/下三角矩阵。
例如,如果我们使用带有上三角矩阵的形式,则基本方程组A ● x = b的解变成 U H ●U ●x = b 。 设置 y = U ●x 并求解系统 U H ●y = b 对于y 通过正向替换。 现在我们有了一个新的三角系统 U ●x = y ,我们可以通过逆向替换来求解x。
为了用这种技术执行这样的系统的解决方案,我们首先通过使用函数cholesky,cho_factor或cholesky_banded来计算因式分解。 然后将输出用于求解器cho_solve。
对于 Cholesky 分解,称为cholesky,cho_factor和cholesky_banded的三个相关函数具有与solveh_banded相似的一组选项。 他们接受一个较低的附加布尔选项(默认设置为False),该选项决定是输出较低的三角形分解还是较高的三角形分解。 函数cholesky_banded需要AB格式的矩阵作为输入。
现在让我们使用所有三种方法测试矩阵B的 Cholesky 分解:
In [8]: B = spsp.diags([[-1]*5, [2]*6, [-1]*5], [-1,0,1]).todense()
...: print B
[[ 2\. -1\. 0\. 0\. 0\. 0.]
[-1\. 2\. -1\. 0\. 0\. 0.]
[ 0\. -1\. 2\. -1\. 0\. 0.]
[ 0\. 0\. -1\. 2\. -1\. 0.]
[ 0\. 0\. 0\. -1\. 2\. -1.]
[ 0\. 0\. 0\. 0\. -1\. 2.]]
In [9]: np.set_printoptions(suppress=True, precision=3)
In [10]: print spla.cholesky(B)
[[ 1.414 -0.707 0\. 0\. 0\. 0\. ]
[ 0\. 1.225 -0.816 0\. 0\. 0\. ]
[ 0\. 0\. 1.155 -0.866 0\. 0\. ]
[ 0\. 0\. 0\. 1.118 -0.894 0\. ]
[ 0\. 0\. 0\. 0\. 1.095 -0.913]
[ 0\. 0\. 0\. 0\. 0\. 1.08 ]]
In [11]: print spla.cho_factor(B)[0]
[[ 1.414 -0.707 0\. 0\. 0\. 0\. ]
[-1\. 1.225 -0.816 0\. 0\. 0\. ]
[ 0\. -1\. 1.155 -0.866 0\. 0\. ]
[ 0\. 0\. -1\. 1.118 -0.894 0\. ]
[ 0\. 0\. 0\. -1\. 1.095 -0.913]
[ 0\. 0\. 0\. 0\. -1\. 1.08 ]]
In [12]: print spla.cholesky_banded(B_banded)
[[ 0\. -0.707 -0.816 -0.866 -0.894 -0.913]
[ 1.414 1.225 1.155 1.118 1.095 1.08 ]]
cho_factor的输出是一个元组:第二个元素是布尔低位。 第一个元素是ndarray,代表方矩阵。 如果将lower设置为True,则在A的 Cholesky 分解中,此ndarray的下三角子矩阵为L。 如果将lower设置为False,则在A的因式分解中,上三角子矩阵为U。 矩阵中的其余元素是随机的,而不是零,因为cho_solve不使用它们。 以类似的方式,我们可以通过cho_banded的输出调用cho_solve_banded来解决适当的系统。
注意
cholesky和cho_factor都是同一个名为potrf的 LAPACK 函数的包装,具有不同的输出选项。 cholesky_banded调用pbtrf。 cho_solve函数是potrs的包装器,cho_solve_banded调用pbtrs。
然后,我们准备使用两个选项之一来解决系统问题:
In [13]: spla.cho_solve((spla.cholesky(B), False), np.ones(6))
Out[13]: array([ 3., 5., 6., 6., 5., 3.])
In [13]: spla.cho_solve(spla.cho_factor(B), np.ones(6))
Out[13]: array([ 3., 5., 6., 6., 5., 3.])
对于任何其他种类的通用方阵A,求解基本系统的下一个最佳方法 A ●x = b 被枢轴化LU分解。 这等效于找到置换矩阵P以及三角矩阵U(上部)和L(下部),以便 P ●A = L ●U 。 在这种情况下,根据P对系统中的行进行置换可以得到等式( P ●A) ●x = P ●b 。 设置c = P ● b和y = U ● x,并使用正向替换在系统 L 中求解y。 然后,在系统 U ●x = y 中用逆向替换求解x。
执行此操作的相关功能是模块scipy.linalg中的lu,lu_factor(用于分解)和lu_solve(用于解决方案)。 对于稀疏矩阵,在模块scipy.sparse.linalg中具有splu和spilu。
让我们首先开始进行因式分解实验。 在此示例中,我们使用大循环矩阵(非对称):
In [14]: D = spla.circulant(np.arange(4096))
In [15]: %timeit spla.lu(D)
1 loops, best of 3: 7.04 s per loop
In [16]: %timeit spla.lu_factor(D)
1 loops, best of 3: 5.48 s per loop
注意
lu_factor函数是 LAPACK 中所有*getrf例程的包装。 lu_solve函数是getrs的包装器。
函数lu具有一个额外的布尔选项:permute_l(默认设置为False)。 如果设置为True,则该功能仅输出两个矩阵 PL = P ●L (正确排列的下三角矩阵)和U。 否则,输出是该顺序的三元组P,L和U。
In [17]: P, L, U = spla.lu(D)
In [17]: PL, U = spla.lu(D, permute_l=True)
函数lu_factor的输出节省资源。 我们获得矩阵LU,其中上三角U和下三角L。 我们还获得了整数dtype,piv的一维ndarray类,表示表示置换矩阵P的枢轴索引。
In [18]: LU, piv = spla.lu_factor(D)
求解器lu_solve将lu_factor的两个输出,一个右侧矩阵b和可选指示器trans带到要解决的基本系统类型:
In [19]: spla.lu_solve(spla.lu_factor(D), np.ones(4096))
Out[19]: array([ 0., 0., 0., ..., 0., 0., 0.])
提示
此时,我们必须对模块scipy.linalg中的通用功能solve进行注释。 它是LAPACK功能POSV和GESV的包装。 它允许我们输入矩阵A和右侧矩阵b,并指示A是否对称且为正定。 无论如何,例程都会在内部决定要使用的两个分解因子(Cholesky 或透视 LU)中的哪一个,并据此计算解决方案。
对于大的稀疏矩阵,如果它们以 CSC 格式存储,则可以使用模块scipy.sparse.linalg中的函数splu或spilu更有效地执行枢轴LU分解。 这两个函数直接使用SuperLU库。 它们的输出不是一组矩阵,而是一个名为scipy.sparse.linalg.dsolve._superlu.SciPyLUType的 Python 对象。 该对象具有四个属性和一个实例方法:
shape:包含矩阵A形状的 2 元组nnz:矩阵A中非零条目的数量perm_c, perm_r:分别应用于矩阵A的列和行的置换,以获得计算出的LU分解solve:将对象转换为接受ndarray b的函数object.solve(b,trans)和可选描述字符串trans的实例方法。
一个大想法是,处理大量数据时,LU分解中的实际矩阵不像分解背后的主要应用(系统的解决方案)那么重要。 所有执行此操作的相关信息都以最佳方式存储在对象的方法solve中。
splu和spilu之间的主要区别在于,后者计算不完全分解。 有了它,我们可以获得矩阵A的逆的非常好的*似值,并使用矩阵乘法来计算大型系统的解,而这只花费了计算实际解的时间的一小部分。
注意
这两个功能的用法相当复杂。 目的是使用对角矩阵Dr和Dc以及置换矩阵Pr和Pc来计算形式为 Pr * Dr * A * Dc * Pc = L * U 的因式分解。 想法是手动*衡基质A,以便使乘积 B = Dr * A * Dc 比A更好。 如果有可能在并行体系结构中解决此问题,我们可以通过最佳地重新排列行和列来提供帮助。 然后,手动输入置换矩阵Pr和Pc以对B的行和列进行预排序。 所有这些选项都可以提供给splu或spilu。
该算法利用放宽超级节点的想法来减少无效的间接寻址和符号时间(允许使用更高级别的 BLAS 操作)。 我们可以选择确定这些对象的程度,以使算法适合手头的矩阵。
有关算法和所有不同选项的完整说明,最好的参考是《 SuperLU 用户指南》,可在 crd-legacy.lbl.gov/~xiaoye/SuperLU/superlu_ug.pdf 在线找到。
让我们用一个简单的示例来说明这一点,其中不需要对行或列进行置换。 在一个较大的下三角 Pascal 矩阵中,所有偶值条目都变为零,所有奇值条目都变为 1。 将此用作矩阵A。 对于右侧,使用一个 1 的向量:
In [20]: A_csc = spsp.csc_matrix(A, dtype=np.float64)
In [21]: invA = spspla.splu(A_csc)
In [22]: %time invA.solve(np.ones(1024))
CPU times: user: 4.32 ms, sys: 105 µs, total: 4.42 ms
Wall time: 4.44 ms
Out[22]: array([ 1., -0., 0., ..., -0., 0., 0.])
In [23]: invA = spspla.spilu(A_csc)
In [24]: %time invA.solve(np.ones(1024))
CPU times: user 656 µs, sys: 22 µs, total: 678 µs
Wall time: 678 µs
Out[24]: array([ 1., 0., 0., ..., 0., 0., 0.])
注意
将在稀疏矩阵上执行过程的时间与本节开始处在相应矩阵A上的初始solve_triangular过程进行比较。 哪个过程更快?
但是,通常,如果必须解决一个基本系统并且矩阵A很大且稀疏,我们更喜欢使用迭代方法快速收敛到实际解。 当它们收敛时,它们对舍入误差始终不那么敏感,因此在计算量非常大时更适合。
在scipy.sparse.linalg模块中,我们有八种不同的迭代方法,所有这些方法都接受以下参数:
- 任意格式的矩阵
A(矩阵,ndarray,稀疏矩阵,甚至线性运算符!),右侧矢量/矩阵b为ndarray。 - 初步猜测为
x0,为ndarray。 - 公差
l,一个浮点数。 如果连续迭代的差小于此值,则代码停止,最后计算出的值作为解决方案输出。 - 允许的最大迭代次数,maxiter,一个整数。
- 预调节器稀疏矩阵
M应当*似A的逆。 - 每次迭代后调用的当前解矢量
xk的callback函数。
建设者
|
描述
|
| --- | --- |
| bicg | 双共轭梯度迭代 |
| bicgstab | 双共轭梯度稳定迭代 |
| cg | 共轭梯度迭代 |
| cgs | 共轭梯度*方迭代 |
| gmres | 广义最小残差迭代 |
| lgmres | LGMRES 迭代 |
| minres | 最小残差迭代 |
| qmr | 拟最小残差迭代 |
选择正确的迭代方法,良好的初步猜测,尤其是成功的 Preconditioner,本身就是一门艺术。 它涉及学习诸如泛函分析中的运算符或 Krylov 子空间方法之类的主题,这些内容远远超出了本书的范围。 在这一点上,为了满足比较,我们满意地展示了一些简单的示例:
In [25]: spspla.cg(A_csc, np.ones(1024), x0=np.zeros(1024))
Out[25]: (array([ nan, nan, nan, ..., nan, nan, nan]), 1)
In [26]: %time spspla.gmres(A_csc, np.ones(1024), x0=np.zeros(1024))
CPU times: user 4.26 ms, sys: 712 µs, total: 4.97 ms
Wall time: 4.45 ms
Out[26]: (array([ 1., 0., 0., ..., -0., -0., 0.]), 0)
In [27]: Nsteps = 1
....: def callbackF(xk):
....: global Nsteps
....: print'{0:4d} {1:3.6f} {2:3.6f}'.format(Nsteps, \
....: xk[0],xk[1])
....: Nsteps += 1
....:
In [28]: print '{0:4s} {1:9s} {1:9s}'.format('Iter', \
....: 'X[0]','X[1]'); \
....: spspla.bicg(A_csc, np.ones(1024), x0=np.zeros(1024),
....: callback=callbackF)
....:
Iter X[0] X[1]
1 0.017342 0.017342
2 0.094680 0.090065
3 0.258063 0.217858
4 0.482973 0.328061
5 0.705223 0.337023
6 0.867614 0.242590
7 0.955244 0.121250
8 0.989338 0.040278
9 0.998409 0.008022
10 0.999888 0.000727
11 1.000000 -0.000000
12 1.000000 -0.000000
13 1.000000 -0.000000
14 1.000000 -0.000000
15 1.000000 -0.000000
16 1.000000 0.000000
17 1.000000 0.000000
Out[28]: (array([ 1., 0., 0., ..., 0., 0., -0.]), 0)
最小二乘
给定通用矩阵A(不一定是正方形)和右侧矢量/矩阵b,我们寻找矢量/矩阵x,使得表达式 A 的 Frobenius 范数 ●x-b 被最小化。
在scipy中考虑了用数字方式解决此问题的三种主要方法:
- 正态方程
- QR 分解
- 奇异值分解
正态方程
正规方程将最小二乘问题简化为使用对称(不一定是正定)矩阵求解线性方程组的基本系统。 它的速度非常快,但是由于存在舍入错误,因此可能不准确。 基本上,它等于求解系统(A H ●A) ●x = A H ●b 。 这等效于求解 x =(A H ●A) -1 ●A H ●b = pinv(A) ●b 。
让我们以示例显示:
In [29]: E = D[:512,:256]; b = np.ones(512)
In [30]: sol1 = np.dot(spla.pinv2(E), b)
In [31]: sol2 = spla.solve(np.dot(F.T, F), np.dot(F.T, b))
QR 分解
QR 分解将任何矩阵转换为正交/ unit 矩阵Q与正方形上三角矩阵R的乘积 A = Q ●R 。 这使我们无需对任何矩阵求逆即可求解系统(因为 Q H = Q -1 ),因此 A ●x = b 变成 R ●x = Q H ●b ,可通过反取代轻松解决。 请注意,以下两种方法是等效的,因为模式economic报告了最大秩的子矩阵:
In [32]: Q, R = spla.qr(E); \
....: RR = R[:256, :256]; BB = np.dot(Q.T, b)[:256]; \
....: sol3 = spla.solve_triangular(RR, BB)
In [32]: Q, R = spla.qr(E, mode='economic'); \
....: sol3 = spla.solve_triangular(R, np.dot(Q.T, b))
奇异值分解
正规方程和 QR 因式分解这两种方法都可以快速工作,并且只有在A的等级满时才可靠。 如果不是这种情况,则必须使用奇异值分解 A = U ●D ●V H 以及单一矩阵U和V和对角矩阵D,其中对角线上的所有条目均为正值。 这允许快速解决方案 x = V ●D -1 ●U H ● b 。
请注意,下面讨论的两种方法是等效的,因为设置为False的选项full_matrices报告了可能的最小大小的子矩阵:
In [33]: U, s, Vh = spla.svd(E); \
....: Uh = U.T; \
....: Si = spla.diagsvd(1./s, 256, 256); \
....: V = Vh.T; \
....: sol4 = np.dot(V, Si).dot(np.dot(Uh, b)[:256])
In [33]: U, s, Vh = spla.svd(E, full_matrices=False); \
....: Uh = U.T; \
....: Si = spla.diagsvd(1./s, 256, 256); \
....: V = Vh.T; \
....: sol4 = np.dot(V, Si).dot(np.dot(Uh, b))
模块scipy.linalg具有一个实际上使用 SVD 方法执行最小二乘的功能:lstsq。 无需手动转置,求逆和乘法所有需要的矩阵。 它是LAPACK函数GELSS的包装。 它输出所需的解以及计算的残差,有效秩和输入矩阵A的奇异值。
In [34]: sol5, residue, rank, s = spla.lstsq(E, b)
请注意,我们执行的所有计算如何提供彼此非常接*的解决方案(如果不相等!):
In [35]: map(lambda x: np.allclose(sol5,x), [sol1, sol2, sol3, sol4])
Out[35]: [True, True, True, True]
正则化最小二乘
在大稀疏矩阵的情况下,模块scipy.sparse.linalg具有两种用于最小二乘的迭代方法,即lsqr和lsmr,这允许使用更通用的阻尼因子d进行迭代。 我们试图使功能norm(A * x - b, 'f')**2 + d**2 * norm(x, 'f')**2最小化。 用法和参数与我们之前研究的迭代函数非常相似。
其他矩阵方程求解器
下表总结了其余的矩阵方程求解器。 这些例程都没有任何参数可用于性能或内存管理或检查数据的完整性:
|建设者
|
描述
|
| --- | --- |
| solve_sylvester(A, B, Q) | 西尔维斯特方程 |
| solve_continuous_are(A, B, Q, R) | 连续代数 Riccati 方程 |
| solve_discrete_are(A, B, Q, R) | 离散代数 Riccati 方程 |
| solve_lyapunov(A, Q) | 连续李雅普诺夫方程 |
| solve_discrete_lyapunov(A, Q) | 离散 Lyapunov 方程 |
基于特征值的矩阵分解
在此类中,我们对*方矩阵有两种因式分解:频谱分解和 Schur 分解(尽管从技术上讲,频谱分解是 Schur 分解的一种特殊情况)。 两者的目的最初是同时显示一个或几个矩阵的特征值,尽管它们的应用程序有很大不同。
光谱分解
我们考虑以下四种情况:
- 给定一个*方矩阵
A,我们求出某些实数或复数值m(相应的特征值)满足 A●v = m●v 的所有向量v(右特征向量)。 如果所有特征向量都不相同,我们将它们收集为矩阵V的列(恰好是可逆的)。 它们对应的特征值以与对角矩阵D的对角条目相同的顺序存储。 然后我们可以将A理解为乘积 A = V●D●V -1 。 我们将此分解称为普通特征值问题。 - 给定一个*方矩阵
A,我们寻找特征值m满足 v●A = m●v 的所有向量v(左特征向量)。 如前所述,如果所有特征向量都不同,则将它们收集在矩阵V中。 它们的对应特征值收集在对角矩阵D中。 然后可以将矩阵A分解为乘积 A = V●D●V -1 。 我们也将此分解称为普通特征值问题。 特征值与前面的情况相同。 - 在给定具有相同大小的*方矩阵
A和B的情况下,我们寻求满足 m●A●v = n●B●v 的所有向量v(广义右特征向量) 复数值m和n。 当比率 r = n / m 是可计算的时,称为广义特征值。 特征向量被收集为矩阵V的列,其对应的广义特征值r被收集在对角矩阵D中。 然后我们可以通过标识 A = B●V●D●V -1 来实现A和B之间的关系。 我们将此身份称为广义特征值问题。 - 对于与之前相同的情况,如果我们求向量
v(广义左特征向量)以及满足 m●v●A = n●v●B 的值m和n另一个类似的分解。 我们再次将这种分解称为广义特征值问题。
模块scipy.linalg和scipy.sparse.linalg中的以下功能可帮助我们计算特征值和特征向量:
建设者
|
描述
|
| --- | --- |
| scipy.linear.eig(A[, B]) | 普通/广义特征值问题 |
| scipy.linalg.eigvals(A[, B]) | 普通/广义特征值问题的特征值 |
| scipy.linalg.eigh(A[, B]) | 普通/广义特征值问题。 厄米/对称矩阵 |
| scipy.linalg.eigvalsh(A[, B]) | 普通/广义特征值问题的特征值; 厄米/对称矩阵 |
| scipy.linalg.eig_banded(AB) | 普通特征值问题; 厄米/对称带矩阵 |
| scipy.linalg.eigvals_banded(AB) | 普通特征值问题的特征值; 厄米/对称带矩阵 |
| scipy.sparse.linalg.eigs(A, k) | 查找 k 个特征值和特征向量 |
| scipy.sparse.linalg.eigsh(A, k) | 查找k特征值和特征向量; 实对称矩阵 |
| scipy.sparse.linalg.lobpcg(A, X) | 具有可选预处理A对称的普通/广义特征值问题 |
对于矩阵不对称或不带状的任何一种特征值问题,我们使用函数eig,该函数是LAPACK例程GEEV和GGEV(后者用于广义特征值问题)的包装器。 对于仅输出特征值而不输出特征向量的情况,函数eigvals是语法糖。 为了报告我们是否需要左右特征向量,我们使用可选的布尔参数left和right。 默认情况下,left设置为False,right设置为True,因此提供了正确的特征向量。
对于具有非带实对称或 Hermitian 矩阵的特征值问题,我们使用函数eigh,该函数是形式为*EVR,*GVD和*GV的 LAPACK 例程的包装。 我们可以选择使用可选参数eigvals输出任意数量的特征值。 这是一个整数元组,指示所需的最低和最高特征值的索引。 如果省略,则返回所有特征值。 在这种情况下,可以使用基于分而治之技术的更快算法进行计算。 我们可以使用可选的布尔参数turbo(默认设置为False)来指示此选择。
如果仅希望报告特征值,则可以将可选参数eigvals_only设置为True,或使用相应的语法糖eighvals。
我们在scipy.linalg模块中考虑的最后一种情况是带状实对称或 Hermitian 矩阵的特征值问题。 我们使用函数eig_banded,确保输入矩阵为 AB 格式。 该函数是LAPACK例程*EVX的包装。
对于非常大的矩阵,特征值的计算通常在计算上是不可能的。 如果这些大型矩阵稀疏,则可以使用两种迭代算法来计算一些特征值,即隐式重启 Arnoldi 和隐式重启 Lanczos 方法(后者用于对称或 Hermitian 方法) 矩阵)。 模块scipy.sparse.linalg具有两个功能eigs和eigsh,它们是执行它们的ARPACK例程*EUPD的包装。 我们还具有执行另一种迭代算法的函数lobpcg,即局部最优块预处理共轭梯度方法。 该函数接受预处理器,因此有可能更快地收敛到所需的特征值。
我们将通过一个有趣的矩阵Andrews来说明所有这些函数的用法。 它创建于 2003 年,旨在对本征值问题的内存高效算法进行基准测试。 它是大小为 60,000×60,000 和 760,154 个非零条目的实对称稀疏矩阵。 可以从的稀疏矩阵集合中下载 www.cise.ufl.edu/research/sparse/matrices/Andrews/Andrews.html 。
对于此示例,我们以 Matrix Market 格式Andrews.mtx下载了该矩阵。 请注意,矩阵是对称的,并且文件仅提供主对角线上或下方的数据。 收集所有这些信息之后,我们确保也填充上三角形:
In [1]: import numpy as np, scipy.sparse as spsp, \
...: scipy.sparse.linalg as spspla
In [2]: np.set_printoptions(suppress=True, precision=6)
In [3]: rows, cols, data = np.loadtxt("Andrews.mtx", skiprows=14,
...: unpack=True); \
...: rows-=1; \
...: cols-=1
In [4]: A = spsp.csc_matrix((data, (rows, cols)), \
...: shape=(60000,60000)); \
...: A = A + spsp.tril(A, k=1).transpose()
我们首先计算绝对值中最大的前五个特征值。 我们使用选项which='LM'调用函数eigsh。
In [5]: %time eigvals, v = spspla.eigsh(A, 5, which='LM')
CPU times: user 3.59 s, sys: 104 ms, total: 3.69 s
Wall time: 3.13 s
In [6]: print eigvals
[ 69.202683 69.645958 70.801108 70.815224 70.830983]
通过切换到选项which='SM',我们也可以根据绝对值来计算最小的特征值:
In [7]: %time eigvals, v = spspla.eigsh(A, 5, which='SM')
CPU times: user 19.3 s, sys: 532 ms, total: 19.8 s
Wall time: 16.7 s
In [8]: print eigvals
[ 10.565523 10.663114 10.725135 10.752737 10.774503]
提示
ARPACK中的例程在查找较小特征值时效率不高。 在这种情况下,通常最好采用移位反转模式以获得更好的性能。 有关此过程的信息,请阅读 www.caam.rice.edu/software/ARPACK/UG/node33.html 中的描述,或 RB Lehoucq,DC Sorensen 和 C.Yang,ARPACK 的文章。 用户指南:通过隐式重新启动的 Arnoldi 方法解决大规模特征值问题。 SIAM,宾夕法尼亚州费城,1998 年。
注意
函数eigsh允许我们通过指示接*所需特征值的值来执行移位反转模式。 如果我们有很好的猜测(如上一步所述),则可以将此过程与选项sigma结合使用,并将策略与选项模式结合使用。 在这种情况下,我们还需要提供线性运算符而不是矩阵。 执行时间要慢得多,但结果通常要精确得多(尽管给出的示例并不建议这样做!)。
In [9]: A = spspla.aslinearoperator(A)
In [10]: %time spspla.eigsh(A, 5, sigma=10.0, mode='cayley')
CPU times: user 2min 5s, sys: 916 ms, total: 2min 6s
Wall time: 2min 6s
In [11]: print eigvals
[ 10.565523 10.663114 10.725135 10.752737 10.774503]
舒尔分解
有四种情况:
- 具有复系数的方阵
A的复数 Schur 分解。 我们可以将A理解为 the 矩阵U与上三角矩阵T的乘积 A = U●T●U H 和U的 Hermitian 转置。 我们称T为A的复杂 Schur 形式。T的对角线中的条目是A的特征值。 - 具有实系数的方阵
A的实数舒尔分解。 如果矩阵的所有特征值均为实值,则可以将矩阵实现为正交矩阵的乘积 A = V●S●V TV和一个上三角矩阵S以及V的转置。S中的块的大小为 1×1 或 2×2。如果块为 1×1,则该值是A的实际特征值之一。 任何 2×2 块代表A的一对复共轭特征值。 我们称S为A的真正 Schur 形式。 - 两个*方矩阵
A和B的复数广义 Schur 分解。 我们可以同时将它们分解为以下形式: A = Q●S●Z H 和 B = Q●T●Z H 具有相同的单一矩阵Q和Z。 矩阵S和T均为上三角,它们对角线元素的比率恰好是A和B的广义特征值。 - 两个实值*方矩阵
A和B的实数广义 Schur 分解。 两者的同时分解可以以 A = Q●S●Z T 和 B = Q●T●Z 的形式实现 T 用于相同的正交矩阵Q和Z。 矩阵S和T是块上三角的,块的大小分别为 1×1 和 2×2。借助这些块,我们可以找到A和B的广义特征值。
模块scipy.linalg 中有四个功能,可为我们提供用于计算以下任何分解的工具:
建设者
|
描述
|
| --- | --- |
| scipy.linalg.schur(A) | 矩阵的舒尔分解 |
| scipy.linalg.rsf2csf(T, Z) | 从真实的 Schur 形式转换为复杂的 Schur 形式 |
| scipy.linalg.qz(A, B) | 两个矩阵的广义 Schur 分解 |
| scipy.linalg.hessenberg(A) | 矩阵的 Hessenberg 形式 |
函数hessenberg 为我们提供了任何 Schur 分解的第一步。 这是形式为 A = Q●U●Q H 的任何方阵A的因式分解,其中Q是一元的,U是上 Hessenberg 矩阵(所有条目在对角线以下均为零)。 该算法基于LAPACK例程GEHRD和GEBAL(用于计算U)和BLAS例程GER和GEMM(用于计算Q)的组合。
函数schur和qz是LAPACK例程GEES和GGES的包装器,用于分别计算*方矩阵的标准和广义 Schur 分解。 我们根据可选参数输出(我们将其设置为'real'或'complex')选择报告复杂分解还是实分解。 我们还可以对矩阵表示中的特征值进行排序。 我们使用可选参数sort进行操作,并具有以下可能性:
None:如果我们不需要任何排序。 这是默认值。'lhp':在左侧*面中。'rhp':在右侧*面'iuc':单位圆内'ouc':单位圆以外func:任何称为func的可调用函数均可用于为用户提供自己的排序
摘要
在本章中,我们探讨了数值线性代数的基本原理,这是科学计算中所有过程的核心。 首先将重点放在矩阵和线性运算符的存储以及基本操作上。 我们详细探讨了所有不同的因式分解,重点放在它们的用法上,以找到矩阵方程或特征值问题的解决方案。 在本章中,我们着重将模块scipy.linalg和scipy.sparse的功能链接到库BLAS,LAPACK,ARPACK和SuperLU中的相应例程。 对于我们的实验,我们从现实问题中选择了有趣的矩阵,这些问题是从佛罗里达大学主办的广泛的稀疏矩阵集合中收集的。
在下一章中,我们将解决插值和最小二乘*似的问题。
二、插值和*似
*似理论指出了如何从某个预定类中的另一个函数中找到给定函数的最佳*似,以及这种*似的效果如何。 在本章中,我们将通过两个设置来探索该字段:插值和最小二乘*似。
动机
考虑一个气象实验,该实验测量海上矩形矩形网格上一组浮标的温度。 我们可以通过指示 16×16 位置的网格上的浮标的经度和纬度,以及它们上的随机温度在 36ºF 至 46ºF 之间来模拟这样的实验:
In [1]: import numpy as np, matplotlib.pyplot as plt, \
...: matplotlib.cm as cm; \
...: from mpl_toolkits.basemap import Basemap
In [2]: map1 = Basemap(projection='ortho', lat_0=20, lon_0=-60, \
...: resolution='l', area_thresh=1000.0); \
...: map2 = Basemap(projection='merc', lat_0=20, lon_0=-60, \
...: resolution='l', area_thresh=1000.0, \
...: llcrnrlat=0, urcrnrlat=45, \
...: llcrnrlon=-75, urcrnrlon=-15)
In [3]: longitudes = np.linspace(-60, -30, 16); \
...: latitudes = np.linspace(15, 30, 16); \
...: lons, lats = np.meshgrid(longitudes, latitudes); \
...: temperatures = 10\. * np.random.randn(16, 16) + 36.
In [4]: x1, y1 = map1(lons, lats); \
...: x2, y2 = map2(lons, lats)
In [5]: plt.rc('text', usetex=True); \
...: plt.figure()
In [6]: plt.subplot(121, aspect='equal'); \
...: map1.drawmeridians(np.arange(0, 360, 30)); \
...: map1.drawparallels(np.arange(-90, 90, 15)); \
...: map1.drawcoastlines(); \
...: map1.fillcontinents(color='coral'); \
...: map1.scatter(x1, y1, 15, temperatures, cmap=cm.gray)
In [7]: plt.subplot(122); \
...: map2.drawmeridians(np.arange(0, 360, 30)); \
...: map2.drawparallels(np.arange(-90, 90, 15)); \
...: map2.drawcoastlines(); \
...: map2.fillcontinents(color='coral'); \
...: C = map2.scatter(x2, y2, 15, temperatures, cmap=cm.gray); \
...: Cb = map2.colorbar(C, "bottom", size="5%", pad="2%"); \
...: Cb.set_label(r'$\mbox{}^{\circ} F$'); \
...: plt.show()
我们获得下图:

可以猜测这些浮标之间的温度(不完全准确,但至少在一定程度上),因为温度是地球表面的光滑函数。 让我们假设,我们需要借助三阶分段 2D 多项式进行*似,并且在块彼此相交处具有最大的*滑度。 当然,一个明显的挑战是浮标不是位于*面上,而是在一个很大的球体的表面上。 对于 SciPy 而言,这不是问题。
In [8]: from scipy.interpolate import RectSphereBivariateSpline \
...: as RSBS
In [9]: soln = RSBS(np.radians(latitudes), \
...: np.pi + np.radians(longitudes), \
...: temperatures)
In [10]: long_t = np.linspace(-60, -30, 180); \
....: lat_t = np.linspace(15, 30, 180); \
....: temperatures = soln(np.radians(lat_t), \
....: np.pi + np.radians(long_t))
In [11]: long_t, lat_t = np.meshgrid(long_t, lat_t); \
....: lo1, la1 = map1(long_t, lat_t); \
....: lo2, la2 = map2(long_t, lat_t)
In [12]: plt.figure()
Out[12]: <matplotlib.figure.Figure at 0x10ec28250>
In [13]: plt.subplot(121, aspect='equal'); \
....: map1.drawmeridians(np.arange(0, 360, 30)); \
....: map1.drawparallels(np.arange(-90, 90, 15)); \
....: map1.drawcoastlines(); \
....: map1.fillcontinents(color='coral'); \
....: map1.contourf(lo1, la1, temperatures, cmap=cm.gray)
Out[13]: <matplotlib.contour.QuadContourSet instance at 0x10f63d7e8>
In [14]: plt.subplot(122); \
....: map2.drawmeridians(np.arange(0, 360, 30)); \
....: map2.drawparallels(np.arange(-90, 90, 15)); \
....: map2.drawcoastlines(); \
....: map2.fillcontinents(color='coral'); \
....: C = map2.contourf(lo2, la2, temperatures, cmap=cm.gray); \
....: Cb = map2.colorbar(C, "bottom", size="5%", pad="2%"); \
....: Cb.set_label(r'$\mbox{}^{\circ} F$'); \
....: plt.show()

我们已经通过简单地通过将温度函数表示为与浮标位置上的温度值相符的分段多项式曲面来解决了这个问题。 这在技术上称为球面上的矩形节点网格上具有双变量样条的插值。
在其他情况下,只要结果函数与实际温度更紧密相关,这些位置的精确值就不是很重要。 在这种情况下,我们不希望执行插值,而是要计算具有相同功能类的元素的*似值。
让我们精确地定义两个设置:
插值问题需要三个要素:
- 有限域上的目标函数
f(x)(为方便起见,我们用x表示)。 - 域中的一组有限点:插值的节点,我们用
xi表示。 我们还将需要在这些节点处评估目标函数(可能还有其某些派生函数)。 在本章中,我们用yi表示这些。 - 插值族:具有与目标函数相同的输入/输出结构的函数。
插值问题的目标是通过匹配节点上目标函数的值,使插值成员对目标函数进行*似。
我们在以下设置中探索插值:
- 最*邻插值(任何维度)
- 通过分段线性函数进行插值(任意维度)
- 通过多项式进行单变量插值(Lagrange 和 Hermite 插值)
- 分段多项式的单变量插值
- 通过样条线进行单变量和双变量插值
- 径向基多元插值
提示
我们假设熟悉样条线的理论和应用。 有很多很好的入门资料,但我们建议您使用一些更实用的口味:
- Carl de Boor,花键实用指南。 施普林格(1978)。
- Paul Dierckx,曲线和样条曲线的曲面拟合。 牛津大学出版社,1993 年。
所有插值均通过模块scipy.interpolate进行。 特别是,与样条相关的是 Paul Dierckx FITPACK库中某些例程的一组包装。
要定义*似问题,我们需要以下四个要素:
- 有限域
x上的目标函数f(x),该函数以尺寸为n的列向量作为输入并输出尺寸为m的列向量 - 一系列*似值
{g[a](x)}:具有与f(x)相同的输入/输出结构,其功能取决于参数a,该参数编码为尺寸为r的列向量 norm是一种用于测量x, ||f(x) - g(x)||任意两个给定功能之间距离的功能
*似问题的目标是找到相对于参数a最小化表达式||f(x) - g[a](x)||的*似成员。 这等效于相对于a为error功能F(a) = ||f(x) - g[a](x)||找到a(局部或全局)最小值。
我们说,如果*似族是一个基本元素的线性组合并且参数a作为系数,则*似是线性; 否则,我们将*似值称为非线性。
在本章中,我们将介绍以下设置中的*似函数:
- 通用线性最小二乘*似(通过求解线性方程组)
- 单变量和二变量样条的最小二乘*似/*滑
- 球上矩形网格上的样条最小二乘*似/*滑
- 通用非线性最小二乘*似(使用 Levenberg-Marquardt 迭代算法)
函数上下文中的最小二乘*似通过几个模块执行:
- 对于一般的线性最小二乘*似,可以始终将问题简化为线性方程组的解。 在这种情况下,我们在上一章研究的
scipy.linalg和scipy.sparse.linalg模块拥有我们需要的所有算法。 如前所述,所需的函数是 Fortran 库BLAS和LAPACK和C库SuperLU中几个例程的包装。 - 对于通过样条线进行线性最小二乘*似的特殊情况,
scipy.interpolate模块执行许多功能(针对所有不同情况),这些功能又是 Paul Diercks 的 Fortran 库FITPACK中的例程包装。 - 对于非线性最小二乘*似,我们使用来自
scipy.optimize模块的函数。 这些函数是 Fortran 库MINPACK中LMDIF和LMDER例程的包装。
提示
有关这些 Fortran 库的更多信息,可以从 Netlib 存储库中的它们的页面中获得很好的参考:
- FITPACK: http://netlib.org/dierckx/
- MINPACK: http://netlib.org/minpack/
- FFTPACK: http://netlib.org/fftpack/
可以从的创建者处找到SuperLU的最佳参考文献,网址为 http://crd-legacy.lbl.gov/~xiaoye/SuperLU/ 。
插补
我们有三种不同的实现方法来处理插值问题:
- 程序模式,该模式计算代表实际解决方案的一组数据点(以
ndarray的形式,具有所需的维数)。 - 在某些特殊情况下,功能性模式为我们提供了代表解决方案的
numpy功能。 - 面向对象的模式,为插值问题创建类。 不同的类具有不同的方法,具体取决于特定种类的插值器所享受的操作。 这种模式的优势在于,通过这些方法,我们可以从解决方案中获取更多信息:不仅是评估或表示,还包括诸如搜索根,计算导数和反导数,错误检查以及计算系数和结之类的相关操作。
代表我们的插值词的方式的选择取决于我们,这主要取决于我们需要多少精度,以及之后需要的信息/操作。
实施细节
在过程模式下,没有太多要添加到实现细节中的内容了。 对于每个插值问题,我们选择一个例程,向该例程中馈送节点xi,这些节点上目标函数(及其可能的导数)的值yi以及要评估插值的域x 。 在某些情况下,如果插值器需要更多结构,我们将提供更多信息。
功能实现甚至更简单:可用时,它们仅需要节点xi的值和在这些节点上的评估yi。
有几种通用的面向对象的插值类。 我们很少篡改它们,而是使用例程在内部创建和操作更合适的子类。 让我们简要介绍一下这些对象:
- 对于通用单变量插值,我们具有
_Interpolator1D类。 可以使用节点集xi以及这些节点上的目标函数的值yi进行初始化。 如有必要,我们也可以使用._set_dtype类方法强制使用yi的数据类型。 如果需要处理插值器的导数,可以使用_Interpolator1DWithDerivatives子类,并使用额外的类方法.derivatives来计算微分的评估。 - 对于样条小于或等于 5 的单变量插值,我们具有
InterpolatedUnivariateSpline类,而该类又是UnivariateSpline类的子类。 它们都是非常丰富的类,具有许多方法不仅可以评估样条曲线或其任何派生类,而且可以计算其派生类和反派生类的样条表示形式。 我们也有方法计算两个点之间的定积分。 也有一些方法可以返回结的位置,样条系数,残差甚至根。 我们至少使用节点xi和适合这些节点yi的值来初始化UnivariateSpline类中的对象。 我们可以选择使用样条曲线的度数初始化对象。 - 对于具有非结构化节点(节点不一定在矩形网格上)的双变量插值,
interp2d类是一个选项,该类可使用 1、3 或 5 阶双变量样条在二维中实现插值。 及其评估。 - 对于在矩形网格上具有节点的双变量样条插值,我们具有
RectBivariateSpline类(当与s = 0参数一起使用时),它是BivariateSpline类的子类。 反过来,BivariateSpline是基类_BivariateSplineBase的子类。 作为其单变量对应物,这是一个非常丰富的类,提供了许多用于评估,提取节点和系数以及计算体积积分或残差的方法。 - 对于多元插值,存在
NDInterpolatorBase类,具有三个子类:NearestNDInterpolator(用于最*邻插值),LinearNDInterpolator(用于分段线性插值)和CloughTocher2DInterpolator(实现分段三次,C1 *滑) ,二维的最小化曲率插值)。 - 为了在球面上的矩形网格上的一组节点上进行插值,需要使用
SphereBivariateSpline类的RectSphereBivariateSpline子类。 我们使用表示球体上节点位置的角度(theta和phi)以及相应的评估来初始化它。 - 对于具有径向函数的多元插值,我们具有
Rbf类。 它相当干燥,因为它仅允许使用评估方法。 它使用节点和评估进行初始化。
单变量插值
下表总结了用 SciPy 编码的不同单变量插值模式,以及我们可能用来解决它们的过程:
|插补模式
|
面向对象的实现
|
程序执行
|
| --- | --- | --- |
| 最*邻居 | interp1d(,kind='nearest') | |
| 拉格朗日多项式。 | BarycentricInterpolator | barycentric_interpolate |
| 埃尔米特多项式。 | KroghInterpolator | krogh_interpolate |
| 分段多项式。 | PiecewisePolynomial | piecewise_polynomial_interpolate |
| 分段线性 | interp1d(,kind='linear') | |
| 通用样条插值 | InterpolatedUnivariateSpline | splrep |
| 零阶样条 | interp1d(,kind='zero') | |
| 线性样条 | interp1d(,kind='slinear') | |
| 二次样条 | interp1d(,kind='quadratic') | |
| 三次样条 | interp1d(,kind='cubic') | |
| 邮编 | PchipInterpolator | pchip_interpolate |
最*邻插值
在一维函数的上下文中,最*邻插值提供了一种解决方案,该解决方案在由节点集的两个连续中点定义的每个子间隔上,围绕每个节点保持不变。 为了计算所需的插值值,我们使用kind='nearest'选项调用通用的scipy.interpolate.interp1d函数。 它仅使用可用的评估方法生成_Interpolator1D类的实例。
以下示例显示了其简单三角函数f(x) = sin(3*x)在 0 到 1 区间上的结果:
In [1]: import numpy as np, matplotlib.pyplot as plt; \
...: from scipy.interpolate import interp1d
In [2]: nodes = np.linspace(0, 1, 5); \
...: print nodes
[ 0\. 0.25 0.5 0.75 1\. ]
In [3]: def f(t): return np.sin(3 * t)
In [4]: x = np.linspace(0, 1, 100) # the domain
In [5]: interpolant = interp1d(nodes, f(nodes), kind='nearest')
In [6]: plt.rc('text', usetex=True)
...: plt.figure(); \
...: plt.axes().set_aspect('equal'); \
...: plt.plot(nodes, f(nodes), 'ro', label='Nodes'); \
...: plt.plot(x, f(x), 'r-', label=r'f(x)=\sin(3x)'); \
...: plt.plot(x, interpolant(x), 'b--', label='Interpolation'); \
...: plt.title("Nearest-neighbor approximation"); \
...: plt.ylim(-0.05, 1.05); \
...: plt.xlim(-0.5, 1.05); \
...: plt.show()
这将产生以下图形:

拉格朗日插值
在拉格朗日插值中,我们寻求与节点集处的目标函数相符的多项式。 在scipy.interpolate模块中,我们有三种方法可以解决此问题:
_Interpolator1D的BarycentricInterpolator子类基于有理函数*似实现了一种非常稳定的算法。 此类有一种评估方法,以及两种添加/更新运行中节点的方法:.add_xi和.set_yi。- 一种程序方案
barycentric_interpolate是上一类的语法糖,评估方法适用于规定的领域。 - 数值不稳定的函数方案
lagrange计算插值多项式的numpy.poly1d实例。 如果节点很少且明智地选择,则此方法可以使我们可靠地处理与目标函数相关的派生,积分和根求解问题。
让我们在臭名昭著的Runge示例中尝试这种插值模式:为函数 f(x)= 1 /(1 + x 2 [ )在从-5 到 5 的间隔中,具有两组相等分布的节点:
In [7]: from scipy.interpolate import BarycentricInterpolator, \
...: barycentric_interpolate, lagrange
In [8]: nodes = np.linspace(-5, 5, 11); \
...: x = np.linspace(-5,5,1000); \
...: print nodes
[-5\. -4\. -3\. -2\. -1\. 0\. 1\. 2\. 3\. 4\. 5.]
In [9]: def f(t): return 1\. / (1\. + t**2)
In [10]: interpolant = BarycentricInterpolator(nodes, f(nodes))
In [11]: plt.figure(); \
....: plt.subplot(121, aspect='auto'); \
....: plt.plot(x, interpolant(x), 'b--', \
....: label="Lagrange Interpolation"); \
....: plt.plot(nodes, f(nodes), 'ro', label='nodes'); \
....: plt.plot(x, f(x), 'r-', label="original"); \
....: plt.legend(loc=9); \
....: plt.title("11 equally distributed nodes")
Out[11]: <matplotlib.text.Text at 0x10a5fbe50>
BarycentricInterpolator类允许以最佳方式添加额外的节点并更新插值,而无需从头开始进行重新计算:
In [12]: newnodes = np.linspace(-4.5, 4.5, 10); \
....: print newnodes
[-4.5 -3.5 -2.5 -1.5 -0.5 0.5 1.5 2.5 3.5 4.5]
In [13]: interpolant.add_xi(newnodes, f(newnodes))
In [14]: plt.subplot(122, aspect='auto'); \
....: plt.plot(x, interpolant(x), 'b--', \
....: label="Lagrange Interpolation"); \
....: plt.plot(nodes, f(nodes), 'ro', label='nodes'); \
....: plt.plot(x, f(x), 'r-', label="original"); \
....: plt.legend(loc=8); \
....: plt.title("21 equally spaced nodes"); \
....: plt.show()
我们获得以下结果:

Runge示例显示了非常简单的插值的缺点之一。 尽管插值器可以在区间内部精确地*似函数,但它在端点处显示出非常大的偏差。
可以调用初始化插值的相同方法来请求有关它们的信息。 以下简短会议说明了这一点:
In [15]: print interpolant.xi
[-5\. -4\. -3\. -2\. -1\. 0\. 1\. 2\. 3\. 4\. 5\. -4.5 -3.5
–2.5 -1.5 -0.5 0.5 1.5 2.5 3.5 4.5]
In [16]: print interpolant.yi.squeeze()
[ 0.04 0.06 0.1 0.2 0.5 1\. 0.5 0.2 0.1 0.06 0.04
0.05 0.08 0.14 0.31 0.8 0.8 0.31 0.14 0.08 0.05]
程序方案具有更简单的语法,但是缺乏动态更新节点的灵活性:
In [17]: y = barycentric_interpolate(nodes, f(nodes), x)
该功能方案还具有简单的语法:
In [18]: g = lagrange(nodes, f(nodes)); \
....: print g
10 9 8 7 6
3.858e-05 x + 6.268e-19 x - 0.002149 x + 3.207e-17 x + 0.04109 x
5 4 3 2
+ 5.117e-17 x - 0.3302 x - 2.88e-16 x + 1.291 x - 1.804e-16 x
埃尔米特插值
Hermite插值的目的是计算与目标函数及其在一组有限节点中的某些导数一致的多项式。 我们通过两种方案以数字方式完成此任务:
_Interpolator1DWithDerivatives的子类KroghInterpolator,具有.derivative方法来计算插值的任何导数的表示形式,并具有.derivatives方法来对其进行评估。krogh_interpolate函数,是上一类的语法糖,并且在规定的域上应用了评估方法。
让我们以伯恩斯坦的示例展示这些例程:在从-1到1的区间中,使用十个*均分布的节点,将 Hermite 插值计算为绝对值函数,并在每个节点上提供一个导数。
提示
节点需要以递增顺序进行馈送。 对于每个我们要派生导数的节点,我们都会根据需要重复该节点多次。 对于xi中节点的每次出现,我们将yi上的函数及其派生子的评估放在相同的入口级别。
In [19]: from scipy.interpolate import KroghInterpolator
In [20]: nodes = np.linspace(-1, 1, 10); \
....: x = np.linspace(-1, 1, 1000)
In [21]: np.set_printoptions(precision=3, suppress=True)
In [22]: xi = np.repeat(nodes, 2); \
....: print xi; \
....: yi = np.ravel(np.dstack((np.abs(nodes), np.sign(nodes)))); \
....: print yi
[-1\. -1\. -0.778 -0.778 -0.556 -0.556 -0.333 -0.333 -0.111
-0.111 0.111 0.111 0.333 0.333 0.556 0.556 0.778 0.778
1\. 1\. ]
[ 1\. -1\. 0.778 -1\. 0.556 -1\. 0.333 -1\. 0.111
-1\. 0.111 1\. 0.333 1\. 0.556 1\. 0.778 1\.
1\. 1\. ]
In [23]: interpolant = KroghInterpolator(xi, yi)
In [24]: plt.figure(); \
....: plt.axes().set_aspect('equal'); \
....: plt.plot(x, interpolant(x), 'b--', \
....: label='Hermite Interpolation'); \
....: plt.plot(nodes, np.abs(nodes), 'ro'); \
....: plt.plot(x, np.abs(x), 'r-', label='original'); \
....: plt.legend(loc=9); \
....: plt.title('Bernstein example'); \
....: plt.show()
这给出了下图:

分段多项式插值
通过规定几个多项式的阶数和节点的有限集,我们可以构造一个插值器,该插值器在两个连续节点之间的每个子间隔上具有所需顺序的多项式弧。 我们可以通过以下步骤构造具有此特征的插值值:
PiecewisePolynomial的子类PiecewisePolynomial,具有评估插值值及其派生值或附加新节点的方法- 对于分段线性插值的特殊情况,
interp1d实用程序仅使用评估方法创建_Interpolator1D类的实例 piecewise_polynomial_interpolate函数,它是PiecewisePolynomial类的语法糖,其评估方法适用于规定的域
让我们回顾一下本节中的第一个示例。 首先,我们尝试使用interp1d进行分段线性插值。 其次,我们使用PiecewisePolynomial在每个节点上应用具有正确导数的分段二次插值(所有分段的阶数均为 2)。
In [25]: from scipy.interpolate import PiecewisePolynomial
In [26]: nodes = np.linspace(0, 1, 5); \
....: x = np.linspace(0, 1, 100)
In [27]: def f(t): return np.sin(3 * t)
In [28]: interpolant = interp1d(nodes, f(nodes), kind='linear')
In [29]: plt.figure(); \
....: plt.subplot(121, aspect='equal'); \
....: plt.plot(x, interpolant(x), 'b--', label="interpolation"); \
....: plt.plot(nodes, f(nodes), 'ro'); \
....: plt.plot(x, f(x), 'r-', label="original"); \
....: plt.legend(loc=8); \
....: plt.title("Piecewise Linear Interpolation")
Out[29]: <matplotlib.text.Text at 0x107be0390>
In [30]: yi = np.zeros((len(nodes), 2)); \
....: yi[:,0] = f(nodes); \
....: yi[:,1] = 3 * np.cos(3 * nodes); \
....: print yi
[[ 0\. 3\. ]
[ 0.682 2.195]
[ 0.997 0.212]
[ 0.778 -1.885]
[ 0.141 -2.97 ]]
In [31]: interpolant = PiecewisePolynomial(nodes, yi, orders=2)
In [32]: plt.subplot(122, aspect='equal'); \
....: plt.plot(x, interpolant(x), 'b--', label="interpolation"); \
....: plt.plot(nodes, f(nodes), 'ro'); \
....: plt.plot(x, f(x), 'r-', label="original"); \
....: plt.legend(loc=8); \
....: plt.title("Piecewise Quadratic interpolation"); \
....: plt.show()
这给出了下图:

在此图像中,分段二次插值和原始函数实际上是无法区分的。 我们需要去计算差值的绝对值(函数以及它的一阶和二阶导数)才能真正实现计算误差。 以下是*似这些误差的粗略计算,并说明了.derivatives方法的使用:
In [33]: np.abs(f(x) - interpolant(x)).max()
Out[33]: 0.0093371930045896279
In [34]: f1prime = lambda t: 3 * np.cos(3 * t); \
....: np.abs(f1prime(x) - interpolant.derivatives(x)).max()
Out[34]: 10.589218385920123
In [35]: f2prime = lambda t: -9 * np.sin(3 * x); \
....: np.abs(f2prime(x) - interpolant.derivatives(x,der=2)).max()
Out[35]: 9.9980773091170505
分段多项式*似的一个很大优点是在不同子区间上使用不同次数的多项式的灵活性。 例如,对于相同的节点集,我们可以在第一个和最后一个子间隔和三次中使用其他行:
In [36]: interpolant = PiecewisePolynomial(nodes, yi, \
....: orders=[1,3,3,1])
实现这种插值方案的另一个巨大优势是,我们可以轻松添加新节点,而无需从头开始进行重新计算。 例如,要在最后一个节点之后添加新节点,我们发出:
In [37]: interpolant.append(1.25, np.array([f(1.25)]))
样条插值
单变量样条曲线是分段多项式的一种特殊情况。 它们在多项式连接的地方具有高度的*滑度。 可以将这些函数编写为基本样条的线性组合,并且对于给定的度数,*滑度和节点集,只需很少的支持即可。
通过interp1d功能和适当的kind选项,可以使用最多 5 个等级的splines使用单变量样条插值。 此函数使用相应的类方法创建_Interpolator1DWithDerivatives的实例。 通过对 Fortran 库FITPACK中的例程的调用来执行计算。 以下示例显示了不同的可能性:
In [38]: splines = ['zero', 'slinear', 'quadratic', 'cubic', 4, 5]; \
....: g = KroghInterpolator([0,0,0,1,1,1,2,2,2,3,3,3], \
....: [10,0,0,1,0,0,0.25,0,0,0.125,0,0]); \
....: f = lambda t: np.log1p(g(t)); \
....: x = np.linspace(0,3,100); \
....: nodes = np.linspace(0,3,11)
In [39]: plt.figure()
In [40]: for k in xrange(6):
....: interpolant = interp1d(nodes, f(nodes), \
....: kind = splines[k])
....: plt.subplot(2,3,k+1, aspect='equal')
....: plt.plot(nodes, f(nodes), 'ro')
....: plt.plot(x, f(x), 'r-', label='original')
....: plt.plot(x, interpolant(x), 'b--', \
....: label='interpolation')
....: plt.title('{0} spline'.format(splines[k]))
....: plt.legend()
In [41]: plt.show()
这给出了下图:

注意
零样条非常类似于最*邻*似,尽管在这种情况下,插值在两个连续节点的每个选择之间都是恒定的。 线性样条曲线与分段线性插值完全相同。 但是,通过样条曲线执行插值的算法较慢。
对于任何给定的问题设置,都有许多不同的样条插值,它们具有相同的度数,节点和评估。 例如,输出还取决于结的位置和数量。 不幸的是,interp1d功能仅允许控制节点和值。 该算法在结计算方面使用了最简单的设置。
例如,请注意,上一个示例中的三次样条插值不能保留目标函数的单调性。 在这种情况下,可以通过谨慎地限制导数或结的位置来强制插值的单调性。 我们有一个特殊功能,可以通过使用 Fritsch-Carlson 算法实现的分段单调三次 Hermite 插值( PCHIP )来完成此任务。 这个简单的算法可以通过_Interpolator1DWithDerivatives的PchipInterpolator子类来实现,也可以通过其等效的过程函数pchip_interpolate来实现。
In [42]: from scipy.interpolate import PchipInterpolator
In [43]: interpolant = PchipInterpolator(nodes, f(nodes))
In [44]: plt.figure(); \
....: plt.axes().set_aspect('equal'); \
....: plt.plot(nodes, f(nodes), 'ro'); \
....: plt.plot(x, f(x), 'r-', label='original'); \
....: plt.plot(x, interpolant(x), 'b--', label='interpolation'); \
....: plt.title('PCHIP interpolation'); \
....: plt.legend(); \
....: plt.show()
这给出了下图:

通用样条插值由InterpolatedUnivariateSpline类处理,在这里我们可以实际控制所有影响样条质量的参数。 在这种情况下,所有计算都由 Fortran 库FITPACK中的例程包装程序执行。 通过scipy.interpolate模块中的一组功能,可以以程序方式访问这些包装器。 下表显示了类方法,相应的过程函数以及它们调用的FITPACK例程之间的匹配:
操作方式
|
面向对象的实现
|
程序
|
FITPACK
|
| --- | --- | --- | --- |
| 插值的实例化 | InterpolatedUnivariateSpline | splrep | CURFIT |
| 报告花键的结 | object.get_knots() | splrep | |
| 报告样条系数 | object.get_coeffs() | splrep | CURFIT |
| 花键的评估 | object() | splev | SPLEV |
| 衍生物 | object.derivative() | splder | |
| 衍生品评估 | object.derivatives() | splev, spalde | SPLDER, SPALDE |
| 反导数 | object.antiderivative() | splantider | |
| 定积分 | object.integral() | splint | SPLINT |
| 根(用于三次样条) | object.roots() | sproot | SPROOT |
提示
所获得的值。 get_coeffs方法是作为 B 样条的线性组合的样条系数。
让我们展示如何通过计算 5 阶相应插值样条曲线的积分来*似估算目标函数图下的面积。
In [45]: from scipy.interpolate import InterpolatedUnivariateSpline \
....: as IUS
In [46]: interpolant = IUS(nodes, f(nodes), k=5)
In [47]: area = interpolant.integral(0,3); \
....: print area
2.14931665485
多元插值
样条曲线的双变量插值可以通过scipy.interpolate模块中的interp2d执行。 这是一个非常简单的类,仅允许使用求值方法,并具有三种基本的样条插值模式编码:线性,三次和五次。 它无法控制节或重。 为了创建双变量样条的表示,interp2d函数从库FITPACK中调用 Fortran 例程SURFIT(可悲的是,实际上并不意味着执行插值!)。 为了用数字方式评估样条,模块调用例程BISPEV。
让我们通过示例显示interp2d的用法。 我们首先构造一个有趣的双变量函数,以在其域上插值 100 个节点的随机选择,并提供可视化效果:
In [1]: import numpy as np, matplotlib.pyplot as plt; \
...: from mpl_toolkits.mplot3d.axes3d import Axes3D
In [2]: def f(x, y): return np.sin(x) + np.sin(y)
In [3]: t = np.linspace(-3, 3, 100); \
...: domain = np.meshgrid(t, t); \
...: X, Y = domain; \
...: Z = f(*domain)
In [4]: fig = plt.figure(); \
...: ax1 = plt.subplot2grid((2,2), (0,0), aspect='equal'); \
...: p = ax1.pcolor(X, Y, Z); \
...: fig.colorbar(p); \
...: CP = ax1.contour(X, Y, Z, colors='k'); \
...: ax1.clabel(CP); \
...: ax1.set_title('Contour plot')
In [5]: nodes = 6 * np.random.rand(100, 2) - 3; \
...: xi = nodes[:, 0]; \
...: yi = nodes[:, 1]; \
...: zi = f(xi, yi)
In [6]: ax2 = plt.subplot2grid((2,2), (0,1), aspect='equal'); \
...: p2 = ax2.pcolor(X, Y, Z); \
...: ax2.scatter(xi, yi, 25, zi) ; \
...: ax2.set_xlim(-3, 3); \
...: ax2.set_ylim(-3, 3); \
...: ax2.set_title('Node selection')
In [7]: ax3 = plt.subplot2grid((2,2), (1,0), projection='3d', \
...: colspan=2, rowspan=2); \
...: ax3.plot_surface(X, Y, Z, alpha=0.25); \
...: ax3.scatter(xi, yi, zi, s=25); \
...: cset = ax3.contour(X, Y, Z, zdir='z', offset=-4); \
...: cset = ax3.contour(X, Y, Z, zdir='x', offset=-5); \
...: ax3.set_xlim3d(-5, 3); \
...: ax3.set_ylim3d(-3, 5); \
...: ax3.set_zlim3d(-4, 2); \
...: ax3.set_title('Surface plot')
In [8]: fig.tight_layout(); \
...: plt.show()
我们获得下图:

然后可以使用以下节点执行分段线性插值:
In [9]: from scipy.interpolate import interp2d
In [10]: interpolant = interp2d(xi, yi, zi, kind='linear')
In [11]: plt.figure(); \
....: plt.axes().set_aspect('equal'); \
....: plt.pcolor(X, Y, interpolant(t, t)); \
....: plt.scatter(xi, yi, 25, zi); \
....: CP = plt.contour(X, Y, interpolant(t, t), colors='k'); \
....: plt.clabel(CP); \
....: plt.xlim(-3, 3); \
....: plt.ylim(-3, 3); \
....: plt.title('Piecewise linear interpolation'); \
....: plt.show()
尽管其名称为interp2d,但此过程不是实际的插值,而是试图拟合数据的粗略*似。 通常,每次运行此代码,都将获得不同质量的结果。 幸运的是,其余的插补例程并非如此!

注意
如果节点的位置不是最优的,我们很可能会收到警告:
Warning: No more knots can be added because the number of B-spline coefficients
already exceeds the number of data points m. Probably causes: either
s or m too small. (fp>s)
kx,ky=1,1 nx,ny=11,14 m=100 fp=0.002836 s=0.000000
提示
注意,在前面的示例中,对插值值的评估是通过调用两个一维数组来执行的。 通常,要评估在矩形网格上用interp2d计算的插值g,该插值可以实现为两个一维数组(大小m的tx和大小n的ty ]),我们发布g(tx, ty); 这给出了一个二维数组,大小为 m x n 。
当然,结果的质量与节点的密度和结构密切相关。 增加其数量或将其位置置于矩形网格上会改善问题。 在节点形成矩形网格的情况下,借助于RectBivariateSpline类可以使用更快,更准确的方法进行实际插值。 此函数是FITPACK库中 Fortran 例程REGRID的包装。
现在让我们在矩形网格上选择 100 个节点并重新计算,如下所示:
In [12]: ti = np.linspace(-3, 3, 10); \
....: xi, yi = np.meshgrid(ti, ti); \
....: zi = f(xi, yi)
In [13]: from scipy.interpolate import RectBivariateSpline
In [14]: interpolant = RectBivariateSpline(ti, ti, zi, kx=1, ky=1)
In [15]: plt.figure(); \
....: plt.axes().set_aspect('equal'); \
....: plt.pcolor(X, Y, interpolant(t, t)); \
....: CP = plt.contour(X, Y, interpolant(t, t), colors='k'); \
....: plt.clabel(CP); \
....: plt.scatter(xi, yi, 25, zi); \
....: plt.xlim(-3, 3); \
....: plt.ylim(-3, 3); \
....: plt.title('Piecewise linear interpolation, \
....: rectangular grid'); \
....: plt.show()
提示
与interp2d的情况一样,要评估在矩形网格上用RectBivariateSpline计算的插值g,该插值可以实现为两个一维数组(m大小为m和[ n大小的ty),我们发布g(tx, ty); 这给出了一个二维数组,大小为 m x n 。
现在给出一个实际的插值:

图下的体积积分非常准确(给定域中目标函数的实际积分为零):
In [16]: interpolant.integral(-3, 3, -3, 3)
Out[16]: 2.636779683484747e-16
在这种情况下,让我们检查一下从此类中获得的一些不同的信息:
-
插值度:
In [17]: interpolant.degrees Out[17]: (1, 1) -
返回的样条曲线*似值的*方残差之和:
In [18]: interpolant.fp Out[18]: 0.0 In [19]: interpolant.get_residual() Out[19]: 0.0 -
插值系数:
In [20]: np.set_printoptions(precision=5, suppress=True) In [21]: print interpolant.get_coeffs() [-0.28224 -0.86421 -1.13653 -0.98259 -0.46831 0.18607 0.70035 0.85429 0.58197 0\. -0.86421 -1.44617 -1.71849 -1.56456 -1.05028 -0.39589 0.11839 0.27232 0\. -0.58197 -1.13653 -1.71849 -1.99082 -1.83688 -1.3226 -0.66821 -0.15394 0\. -0.27232 -0.85429 -0.98259 -1.56456 -1.83688 -1.68294 -1.16867 -0.51428 0\. 0.15394 -0.11839 -0.70035 -0.46831 -1.05028 -1.3226 -1.16867 -0.65439 -0\. 0.51428 0.66821 0.39589 -0.18607 0.18607 -0.39589 -0.66821 -0.51428 -0\. 0.65439 1.16867 1.3226 1.05028 0.46831 0.70035 0.11839 -0.15394 0\. 0.51428 1.16867 1.68294 1.83688 1.56456 0.98259 0.85429 0.27232 0\. 0.15394 0.66821 1.3226 1.83688 1.99082 1.71849 1.13653 0.58197 0\. -0.27232 -0.11839 0.39589 1.05028 1.56456 1.71849 1.44617 0.86421 0\. -0.58197 -0.85429 -0.70035 -0.18607 0.46831 0.98259 1.13653 0.86421 0.28224] -
结的位置:
In [22]: interpolant.get_knots() (array([-3\. , -3\. , -2.33333, -1.66667, -1\. , -0.33333, 0.33333, 1\. , 1.66667, 2.33333, 3\. , 3\. ]), array([-3\. , -3\. , -2.33333, -1.66667, -1\. , -0.33333, 0.33333, 1\. , 1.66667, 2.33333, 3\. , 3\. ]))
使用分段三次样条曲线可以获得更*滑的结果。 在前面的示例中,我们可以通过设置kx = 3和ky = 3来完成此任务:
In [23]: interpolant = RectBivariateSpline(ti, ti, zi, kx=3, ky=3)
In [24]: fig = plt.figure(); \
....: ax = fig.add_subplot(121, projection='3d',aspect='equal'); \
....: ax.plot_surface(X, Y, interpolant(t, t), alpha=0.25, \
....: rstride=5, cstride=5); \
....: ax.scatter(xi, yi, zi, s=25); \
....: C = ax.contour(X, Y, interpolant(t, t), zdir='z', \
....: offset=-4); \
....: C = ax.contour(X, Y, interpolant(t, t), zdir='x',\
....: offset=-5); \
....: ax.set_xlim3d(-5, 3); \
....: ax.set_ylim3d(-3, 5); \
....: ax.set_zlim3d(-4, 2); \
....: ax.set_title('Cubic interpolation, RectBivariateSpline')
在所有可能的三次样条插补中,有一种特殊情况可以最大程度地减小曲率。 通过收敛到解决方案的巧妙的迭代算法,我们可以实现这种特殊情况。 它依赖于以下三个关键概念:
- 使用节点作为顶点的域的 Delaunay 三角剖分
- 使用 Cough-Tocher 方案在每个三角形上支持的 Bezier 三次多项式
- 估计和施加梯度以最小化曲率
可以通过scipy.interpolate模块中的CloughTocher2dInterpolator功能或通过带有method='cubic'选项的同一模块中的黑匣子功能griddata来实现。 让我们比较输出:
In [25]: from scipy.interpolate import CloughTocher2DInterpolator
In [26]: nodes = np.dstack((np.ravel(xi), np.ravel(yi))).squeeze(); \
....: zi = f(nodes[:, 0], nodes[:, 1])
In [27]: interpolant = CloughTocher2DInterpolator(nodes, zi)
In [28]: ax = fig.add_subplot(122, projection='3d', aspect='equal'); \
....: ax.plot_surface(X, Y, interpolant(X, Y), alpha=0.25, \
....: rstride=5, cstride=5); \
....: ax.scatter(xi, yi, zi, s=25); \
....: C = ax.contour(X, Y, interpolant(X, Y), zdir='z', \
....: offset=-4); \
....: C = ax.contour(X, Y, interpolant(X, Y), zdir='x', \
....: offset=-5); \
....: ax.set_xlim3d(-5, 3); \
....: ax.set_ylim3d(-3, 5); \
....: ax.set_zlim3d(-4, 2); \
....: ax.set_title('Cubic interpolation, \
....: CloughTocher2DInterpolator'); \
....: plt.show()
提示
与interp2d和RectBivariateSpline的情况不同,要评估用CloughTocher2DInterpolator在矩形网格X, Y = domain上计算的插值g,我们发布g(X, Y)或g(*domain)。
这给出了下图:

黑盒程序函数griddata还允许我们访问多维的分段线性插值,以及多维最*邻插值。
In [29]: from scipy.interpolate import griddata
In [30]: Z = griddata(nodes, zi, (X, Y), method='nearest')
In [31]: plt.figure(); \
....: plt.axes().set_aspect('equal'); \
....: plt.pcolor(X, Y, Z); \
....: plt.colorbar(); \
....: plt.title('Nearest-neighbors'); \
....: plt.show()
这为我们提供了以下不太令人印象深刻的图表:

还需要考虑另一种插值模式:径向基函数插值。 此处的目的是针对同一函数g插入以(xk, yk)为中心的形式fk(x,y) = g(sqrt((x-xk)**2 + (y-yk)**2))的径向函数的线性组合。 我们可以在七个标准功能g (listed below)中进行选择,甚至可以选择我们自己的:
'multiquadric': g(r) = sqrt((r/self.epsilon)**2 + 1)'inverse': g(r) = 1.0/sqrt((r/self.epsilon)**2 + 1)'gaussian': g(r) = exp(-(r/self.epsilon)**2)'linear': g(r) = r'cubic': g(r) = r**3'quintic': g(r) = r**5'thin_plate': g(r) = r**2 * log(r)
通过Rbf类执行该实现。 可以照常使用节点及其评估值对其进行初始化。 我们还需要包括对径向函数的选择,并且如果需要,还可以包括影响bumps大小的epsilon参数的值。
让我们运行几个插值:首先,通过具有标准偏差epsilon = 2.0的径向高斯曲线,然后使用基于sinc的径向函数。 让我们还回到随机节点:
In [32]: from scipy.interpolate import Rbf
In [33]: nodes = 6 * np.random.rand(100, 2) - 3; \
....: xi = nodes[:, 0]; \
....: yi = nodes[:, 1]; \
....: zi = f(xi, yi)
In [34]: interpolant = Rbf(xi, yi, zi, function='gaussian', \
....: epsilon=2.0)
In [35]: plt.figure(); \
....: plt.subplot(121, aspect='equal'); \
....: plt.pcolor(X, Y, interpolant(X, Y)); \
....: plt.scatter(xi, yi, 25, zi); \
....: plt.xlim(-3, 3); \
....: plt.ylim(-3, 3)
Out[35]: (-3, 3)
In [36]: interpolant = Rbf(xi, yi, zi, function = np.sinc)
In [37]: plt.subplot(122, aspect='equal'); \
....: plt.pcolor(X, Y, interpolant(X, Y)); \
....: plt.scatter(xi, yi, 25, zi); \
....: plt.xlim(-3, 3); \
....: plt.ylim(-3, 3); \
....: plt.show()
尽管节点是非结构化的,但这仍然提供了两个非常准确的插值:

需要考虑的最后一种情况是球体上矩形网格上的二元样条插值。 我们使用RectSphereBivariateSpline函数获得此插值,该函数通过调用FITPACK例程SPGRID(用于计算样条的表示)和BISPEV(用于评估)来实例化SphereBivariateSpline的子类。
实施和评估让人联想到 Fortran 编码方法:
- 要计算样条曲线表示,我们发出
RectSphereBivariateSpline(u, v, data)命令,其中u和v都是严格增加的正值的一维数组,这些正值分别代表了纬度和经度的角度(以弧度为单位)。 节点的位置。 - 在评估时,如果需要在尺寸为 m x n 的细化网格上以二维形式表示插值,我们将发布
object(theta, phi),其中theta和phi是一维且严格增加的,并且必须包含在以上u和v定义的域中。 输出(尽管您的文档说了什么)是 m x n 数组。
最小二乘*似
在数值上,陈述最小二乘范数的*似问题相对简单。 这是本节的主题。
线性最小二乘*似
在线性最小二乘*似的情况下,始终可以通过求解线性方程组来减少问题,如以下示例所示:
考虑从 0 到 1 的区间中的正弦函数 f(x)= sin(x)。我们选择二阶多项式作为*似值: {a 0 +一个 1 x +一个 2 x 2 } 。 为了计算值 [a 0 , 1 一个使这个问题最小化的 2 ] ,我们首先形成一个 3×3 矩阵,其中包含成对的点积(两个乘积的积分 基本功能 {1,x,x 2 } 的基本功能)。 由于此问题的性质,我们获得了阶数为 3 的希尔伯特矩阵:
[ < 1, 1 > < 1, x > < 1, x^2 > ] [ 1 1/2 1/3 ]
[ < x, 1 > < x, x > < x, x^2 > ] = [ 1/2 1/3 1/4 ]
[ < x^2, 1 > < x^2, x > < x^2, x^2 > ] [ 1/3 1/4 1/5 ]
系统的右侧是给定间隔中正弦函数与每个基本函数的点积的列向量:
[ < sin(x), 1 > ] [ 1 - cos(1) ]
[ < sin(x), x > ] = [ sin(1) - cos(1) ]
[ < sin(x), x^2 > ] [ 2*sin(1) + cos(1) - 2 ]
我们按以下方式计算系数和相应的*似多项式:
In [1]: import numpy as np, scipy.linalg as spla, \
...: matplotlib.pyplot as plt
In [2]: A = spla.hilbert(3); \
...: b = np.array([1-np.cos(1), np.sin(1)-np.cos(1), \
...: 2*np.sin(1)+ np.cos(1)-2])
In [3]: spla.solve(A, b)
Out[3]: array([-0.00746493, 1.09129978, -0.2354618 ])
In [4]: poly1 = np.poly1d(spla.solve(A, b)[::-1]); \
...: print poly1
2
-0.2355 x + 1.091 x - 0.007465
通常,要解决基于r元素的线性最小二乘*似问题,我们需要解决具有r方程和r不定式的线性方程组的基本系统。 尽管其表面上很简单,但该方法远非完美。 以下是两个主要原因:
- 该系统可能状况不佳,就像前面的示例一样。
- 系数是非永久性的。 系数的值在很大程度上取决于
r。 增加问题的范围会导致产生一组新系数,与先前的系数不同。
注意
有多种方法可以修复系统的故障。 一种标准程序是使用 Gram-Schmidth 和改进的 Gram-Schmidt 正交化方法从原始对象构建正交基础。 这个主题超出了本专论的范围,但是可以在 Walter Gautschi,Birkhäuser,1997 年的数值分析一书的第 1 章中阅读这些方法的良好参考。
始终提供简单线性系统的基础是 B 样条曲线。 所有涉及的系统都是三对角线的,因此无需复杂的操作即可轻松解决。 scipy.interpolate模块中编码的面向对象系统使我们能够在内部执行所有这些计算。 这是所涉及的类和子类的简要枚举:
UnivariateSpline用于一维的样条曲线或任何维的曲线样条。 我们很少直接使用此类,而是使用LSQUnivariateSpline子类。BivariateSpline用于样条线的曲线,表示放置在矩形上的节点上的曲面。 作为其单变量对应项,不得直接使用此类。 相反,我们利用了LSQBivariateSpline子类。- 样条曲线的
SphereBivariateSpline表示放置在球体上的节点上的曲面。 计算必须改为通过LSQSphereBivariateSpline子类进行。
提示
在这三种情况下,基类及其方法在插值问题中都是相对应的。 有关更多信息,请参见插值部分。
让我们通过一些示例来说明这些面向对象的技术:
在最小二乘意义上,用三次样条(k = 3)在相同的域上*似具有相同的sine函数。 首先,请注意,我们必须提供一个边界框,该域上的一组结点以及最小二乘*似的权重w。 我们还可以提供可选的*滑度参数s。 如果s = 0,则获得插值,而对于s的值较大,则可以实现所得样条曲线的不同程度的*滑度。 为了获得可靠的(加权的)最小二乘*似,较好的选择是s = len(w)(默认情况下,例程会执行此操作)。 还请注意,计算出的误差有多小:
In [5]: f = np.sin; \
...: x = np.linspace(0,1,100); \
...: knots = np.linspace(0,1,7)[1:-1]; \
...: weights = np.ones_like(x)
In [6]: from scipy.interpolate import LSQUnivariateSpline
In [7]: approximant = LSQUnivariateSpline(x, f(x), knots, k=3, \
...: w = weights, bbox = [0, 1])
In [8]: spla.norm(f(x) - approximant(x))
Out[8]: 3.370175009262551e-06
提示
计算此*似误差的更方便方法是使用。 get_residual方法如下:
In [9]: approximant.get_residual()**(.5)
Out[9]: 3.37017500928446e-06
在[-3,3] x [-3,3]域上*似二维函数sin(x)+sin(y)。 我们首先选择域的表示形式,网格上的一组 100 个合适的节点以及权重。 由于LSQBivariateSpline函数的所有输入都必须是一维数组,因此我们在调用*似函数之前执行相应的转换:
In [10]: def f(x, y): return np.sin(x) + np.sin(y); \
....: t = np.linspace(-3, 3, 100); \
....: domain = np.meshgrid(t, t); \
....: X, Y = domain; \
....: Z = f(*domain)
In [11]: X = X.ravel(); \
....: Y = Y.ravel(); \
....: Z = Z.ravel()
In [12]: kx = np.linspace(-3,3,12)[1:-1]; \
....: ky = kx.copy(); \
....: weights = np.ones_like(Z);
In [13]: from scipy.interpolate import LSQBivariateSpline
In [14]: approximant = LSQBivariateSpline(X, Y, Z, kx, kx, \
....: w = weights)
In [15]: approximant.get_residual()
Out[15]: 0.0
提示
也可以使用RectBivariateSpline功能执行此计算。 为了实现最小二乘插值,我们提供了足够大的节点(而不是结,因为它们将自动计算),权重w和*滑度参数s。 s = len(w)是一个不错的选择。
非线性最小二乘*似
在非线性最小二乘*似的情况下,我们通常没有简单矩阵表示的奢侈。 取而代之的是,我们使用迭代过程的两个变体,即scipy.optimize模块中托管的 Levenberg-Marquardt 算法。 可以通过leastsq包装器调用这两个版本,它们对应于 Fortran 库MINPACK中的LMDER和LMDIF例程。
下表列出了此功能的所有选项:
|选项
|
描述
|
| --- | --- |
| func | 错误功能F(a) |
| x0 | 最小化的起始估算值,大小为r |
| args | 作为元组的func的额外参数 |
| Dfun | 表示func的雅可比矩阵的函数 |
| full_output | 布尔型 |
| col_deriv | 布尔型 |
| ftol | 期望的相对误差*方和 |
| xtol | *似解中所需的相对误差 |
| gtol | func和Dfun的列之间需要正交 |
| maxfev | 最大通话次数。 如果为零,则通话次数为100*(r+1) |
| epsfcn | 如果Dfun=None,我们可以指定一个浮点值作为雅可比矩阵的前向差分*似中的步长 |
| factor | 浮点值介于 0.1 到 100 之间,指示初始步长边界 |
| diag | 每个变量的比例因子 |
当我们有一个可靠的误差函数雅可比矩阵时,将使用算法的第一个变体。 如果未提供,则使用该算法的第二种变体,该变体通过前向差*似雅可比行列式。 我们通过几个示例来说明这两种变体。
让我们开始使用该方法重新审视先前的示例,以了解用法和准确性的差异。 我们将把计算重点放在从 0 到 1 的间隔的分区上,该分区具有 100 个均匀间隔的点:
In [16]: from scipy.optimize import leastsq
In [17]: def error_function(a):
....: return a[0] + a[1] * x + a[2] * x**2 - np.sin(x)
In [18]: def jacobian(a): return np.array([np.ones(100), x, x**2])
In [19]: coeffs, success = leastsq(error_function, np.zeros((3,)))
In [20]: poly2 = np.poly1d(coeffs[::-1]); print poly2
2
-0.2354 x + 1.091 x - 0.007232
In [21]: coeffs, success = leastsq(error_function, np.zeros((3,)), \
....: Dfun = jacobian, col_deriv=True)
In [22]: poly3 = np.poly1d(coeffs[::-1]); \
...: print poly3
2
-0.2354 x + 1.091 x - 0.007232
In [23]: map(lambda f: spla.norm(np.sin(x) - f(x)), \
....: [poly1, poly2, poly3])
Out[23]:
[0.028112146265269783, 0.02808377541388009, 0.02808377541388009]
提示
scipy.optimize模块中还有另一个函数可以执行非线性最小二乘*似:curve_fit。 它使用相同的算法,但不是误差函数,而是将其泛型*似值g[a](x)以及自变量x的适当域以及目标函数f的输出传递给该域 。 我们也确实需要输入初始估计。 输出与所需系数一起是所述系数的协方差的估计。
In [23]: from scipy.optimize import curve_fit
In [24]: def approximant(t, a, b, c):
....: return a + b*t + c*t**2
In [25]: curve_fit(approximant, x, np.sin(x), \
....: np.ones((3,)))
(array([-0.007232 , 1.09078356, -0.23537796]),
array([[ 7.03274163e-07, -2.79884256e-06,
2.32064835e-06],
[ -2.79884256e-06, 1.50223585e-05,
-1.40659702e-05],
[ 2.32064835e-06, -1.40659702e-05,
1.40659703e-05]]))
在本节中,我们仅专注于leastsq功能。 这两个功能的目标和编码是相同的,但是leastsq可以按需提供更多信息,并可以更好地控制 Levenberg-Marquardt 算法的不同参数。
现在让我们尝试一些实际的非线性问题:
在第一个示例中,我们将使用有理函数(在每个多项式最多具有 1 的阶数)下从 0 到 1 的区间*似tan(2*x)函数。
In [26]: def error_function(a):
....: return (a[0] + a[1]*x)/(a[2] + a[3]*x) – np.tan(2*x)
In [27]: def jacobian(a):
....: numerator = a[0] + a[1]*x
....: denominator = a[2] + a[3]*x
....: return np.array( [ 1./denominator, x/denominator, \
....: -1.0*numerator/denominator**2, \
....: -1.0*x*numerator/denominator**2 ])
为了显示初始估计的依赖性,我们将尝试三种不同的选择:一种没有意义(所有系数为零),另一种为盲目标准选择(所有条目等于一个),另一种选择 承认tan(2*x)函数具有垂直渐*线这一事实。 我们将假装我们不知道确切的位置,并将其*似为0.78。 然后,我们的第三个初始估计表示一个简单的有理函数,在0.78处有一个渐*线。
显然,错误的初始估算并不能提供任何有用的信息:
In [28]: x1 = np.zeros((4,)); \
....: x2 = np.ones((4,)); \
....: x3 = np.array([1,0,0.78,-1])
In [29]: coeffs, success = leastsq(error_function, x1); \
....: numerator = np.poly1d(coeffs[1::-1]); \
....: denominator = np.poly1d(coeffs[:1:-1]); \
....: print numerator, denominator
0
0
In [30]: coeffs, success = leastsq(error_function, x1, \
....: Dfun=jacobian, col_deriv=True); \
....: numerator = np.poly1d(coeffs[1::-1]); \
....: denominator = np.poly1d(coeffs[:1:-1]); \
....: print numerator, denominator
0
0
使用x2作为初始猜测的这两个*似值都不令人满意:相应的误差很大,并且两个解都不具有从 0 到 1 的区间中的渐*线。
In [31]: coeffs, success = leastsq(error_function, x2); \
....: numerator = np.poly1d(coeffs[1::-1]); \
....: denominator = np.poly1d(coeffs[:1:-1]); \
....: print numerator, denominator; \
....: spla.norm(np.tan(2*x) - numerator(x) / denominator(x))
-9.729 x + 4.28
-1.293 x + 1.986
Out[31]: 220.59056436054016
In [32]: coeffs, success = leastsq(error_function, x2, \
....: Dfun=jacobian, col_deriv=True); \
....: numerator = np.poly1d(coeffs[1::-1]); \
....: denominator = np.poly1d(coeffs[:1:-1]); \
....: print numerator, denominator; \
....: spla.norm(np.tan(2*x) - numerator(x) / denominator(x))
-655.9 x + 288.5
-87.05 x + 133.8
Out[32]: 220.590564978504
使用 x3 作为初始猜测的*似值更接*目标函数,并且两者都具有可接受的渐*线。
In [33]: coeffs, success = leastsq(error_function, x3); \
....: numerator = np.poly1d(coeffs[1::-1]); \
....: denominator = np.poly1d(coeffs[:1:-1]); \
....: print numerator, denominator; \
....: spla.norm(np.tan(2*x) - numerator(x) / denominator(x))
0.01553 x + 0.02421
-0.07285 x + 0.05721
Out[33]: 2.185984698129936
In [34]: coeffs, success = leastsq(error_function, x3, \
....: Dfun=jacobian, col_deriv=True); \
....: numerator = np.poly1d(coeffs[1::-1]); \
....: denominator = np.poly1d(coeffs[:1:-1]); \
....: print numerator, denominator; \
....: spla.norm(np.tan(2*x) - numerator(x) / denominator(x))
17.17 x + 26.76
-80.52 x + 63.24
Out[34]: 2.1859846981334954
当然,我们可以做得更好,但是这些简单的示例现在就足够了。
如果我们希望输出更多信息来监视*似质量,可以将full_output选项设置为True来执行此操作:
In [35]: approximation_info = leastsq(error_function, x3, \
....: full_output=True)
In [36]: coeffs = approximation_info[0]; \
....: print coeffs
[ 0.02420694 0.01553346 0.0572128 -0.07284579]
In [37]: message = approximation_info[-2]; \
....: print message
Both actual and predicted relative reductions in the sum of squares
are at most 0.000000
In [38]: infodict = approximation_info[2]; \
....: print 'The algorithm performed \
....: {0:2d} iterations'.format(infodict['nfev'])
The algorithm performed 97 iterations
尽管从技术上讲,leastsq算法主要处理单变量函数的*似,但可以借助索引,散布,解包(使用特殊的*运算符)和稳定的和来处理多元函数。
提示
使用numpy实例方法sum(或使用numpy函数sum)的ndarray浮点数的总和远非稳定。 我们强烈建议不要将其用于较大数量的数字。 以下示例显示了一种不希望出现的情况,在这种情况下,我们尝试添加 4000 个值:
>>> arr=np.array([1,1e20,1,-1e20]*1000,dtype=np.float64)
>>> arr.sum() # The answer should be, of course, 2000
0.0
为了解决这种情况,我们使用稳定的总和。 在math模块中,为此目的提供了 Shewchuk 算法的实现:
>>> from math import fsum
>>> fsum(arr)
2000.0
有关 Shewchuk 算法的更多信息,以及使用浮点算术进行科学计算时应避免的其他常见陷阱,我们建议使用出色的指南,David Goldberg 着每位计算机科学家应该了解的浮点算术。 。 ACM 计算调查,1991 年。 23,第 5-48 页。
最好用一个例子来说明这个过程。 我们首先生成目标函数:32×32 尺寸的图像,除了三个具有不同位置,方差和高度的球形高斯函数外,还包含白噪声。 我们将所有这些值收集在一个 3×4 的数组中,这些数组称为值。 第一和第二列包含中心坐标的x和y值。 第三列包含高度,第四列包含差异。
In [39]: def sphericalGaussian(x0, y0, h, v):
....: return lambda x,y: h*np.exp(-0.5*((x-x0)**2+(y-y0)**2)/v)
....:
In [40]: domain = np.indices((32, 32)); \
....: values = np.random.randn(3,4); \
....: values[:,:2] += np.random.randint(1, 32, size=(3, 2)); \
....: values[:,2] += np.random.randint(1, 64, size=3); \
....: values[:,3] += np.random.randint(1, 16, size=3); \
....: print values
[[ 17.43247918 17.15301326 34.86691265 7.84836966]
[ 5.5450271 20.68753512 34.41364835 4.78337552]
[ 24.44909459 27.28360852 0.62186068 9.15251106]]
In [41]: img = np.random.randn(32,32)
In [42]: for k in xrange(3):
....: img += sphericalGaussian(*values[k])(*domain)
让我们假设我们不知道中心,高度和方差,并希望从目标图像img进行估计。 然后,我们创建一个误差函数来计算 3×4 数组a中压缩的 12 个系数。 请注意numpy函数 ravel 的作用和实例方法的重塑,以确保正确处理数据:
In [43]: from math import fsum
In [44]: def error_function(a):
....: a = a.reshape(3,4)
....: cx = a[:,0] # x-coords
....: cy = a[:,1] # y-coords
....: H = a[:,2] # heights
....: V = a[:,3] # variances
....: guess = np.zeros_like(img)
....: for i in xrange(guess.shape[0]):
....: for j in xrange(guess.shape[1]):
....: arr = H*np.exp(-0.5*((i-cx)**2+(j-cy)**2)/V)
....: guess[i,j] = fsum(arr)
....: return np.ravel(guess-img)
在保证成功的情况下,在这种情况下开始最小二乘法的过程需要一个*似的初步猜测。 对于此特定示例,我们将从名为values的数组中产生初始猜测:
In [45]: x0 = np.vectorize(int)(values); \
....: print x0
[[17 17 34 7]
[ 5 20 34 4]
[24 27 0 9]]
In [46]: leastsq(error_function, x0)
Out[46]:
(array([ 17.43346907, 17.14219682, 34.82077187, 7.85849653,
5.52511918, 20.68319748, 34.28559808, 4.8010449 ,
25.19824918, 24.02286107, 3.87170006, 0.5289382 ]),
1)
现在让我们通过以下过程直观地比较目标图像img及其最小化:
In [47]: coeffs, success = _; \
....: coeffs = coeffs.reshape(3,4)
In [48]: output = np.zeros_like(img)
In [49]: for k in xrange(3):
....: output += sphericalGaussian(*coeffs[k])(*domain)
In [50]: plt.figure(); \
....: plt.subplot(121); \
....: plt.imshow(img); \
....: plt.title('target image'); \
....: plt.subplot(122); \
....: plt.imshow(output); \
....: plt.title('computed approximation'); \
....: plt.show()
这给出了下图:

摘要
在本章中,我们探讨了*似理论领域中的两个基本问题:最小二乘意义上的插值和*似。 我们了解到有三种不同的方式可以解决 SciPy 中的这些问题:
- 一种程序模式,以
ndarrays的形式提供快速的数值解决方案。 - 提供
numpy功能的功能模式作为输出。 - 面向对象的模式,通过不同的类及其方法具有极大的灵活性。 当我们从解决方案中需要其他信息(例如,有关根,系数,结和错误的信息)或相关对象(例如,导数或反导数的表示形式)时,便使用此模式。
我们详细探讨了scipy.interpolate模块中编码的插值的所有不同实现,并且特别了解到与样条相关的实现是 Fortran 库FITPACK中多个例程的包装。
在最小二乘意义上的线性*似情况下,我们了解到,我们可以通过线性方程组(通过上一章中的技术)来实现解决方案,或者在样条*似的情况下,可以通过包装到 Fortran 例程中的方法来实现。 FITPACK库。 所有这些功能都在scipy.interpolate模块中编码。
对于最小二乘意义上的非线性*似,我们发现了scipy.optimize模块中编码的 Levenberg-Marquardt 迭代算法的两个变体。 这些依次从库MINPACK中调用 Fortran 例程LMDER和LMDIF。
在下一章中,我们将掌握差异化和集成的技术和应用。
三、微分与积分
在本章中,我们将掌握一些经典的和最先进的技术,以执行微积分(以及扩展到物理和每个工程领域)的两个核心操作:功能的微分和集成。
动机
铁路或道路建筑(尤其是高速公路出口)以及许多过山车中那些疯狂的环路的设计共同点是二维或三维微分方程的解决方案,它解决了曲率和向心加速度对运动物体的影响 。 1970 年代,沃纳·斯坦格尔(Werner Stengel)研究并应用了多种模型来解决该问题,在他发现的众多解决方案中,有一种解决方案特别出色-使用回旋环(基于 Cornu 螺旋线的截面)。 1976 年,在美国加利福尼亚州巴伦西亚的六旗魔术山的 Baja Ridge 地区建造了第一个以此范式设计的环形过山车。 它是由美国大革命(Great American Revolution)创造的,它具有第一个垂直环(与两个开瓶器一起,总共三个倒置)。

设计中最棘手的部分是基于微分方程组,其解决方案取决于菲涅耳型正弦和余弦积分的积分,然后选择结果曲线的适当部分。 让我们看看这些有趣的函数的计算和绘图:
In [1]: import numpy as np, matplotlib.pyplot as plt; \
...: from scipy.special import fresnel
In [2]: np.info(fresnel)
fresnel(x[, out1, out2])
(ssa,cca)=fresnel(z) returns the Fresnel sin and cos integrals:
integral(sin(pi/2 * t**2),t=0..z) and
integral(cos(pi/2 * t**2),t=0..z)
for real or complex z.
In [3]: ssa, cca = fresnel(np.linspace(-4, 4, 1000))
In [4]: plt.plot(ssa, cca, 'b-'); \
...: plt.axes().set_aspect('equal'); \
...: plt.show()
结果如下图:

菲涅耳积分的重要性使它们在 SciPy 库中具有永久性的地位。 还有许多其他有用的积分具有相同的命运,并且现在可以在模块scipy.special中进行操作了。 有关所有这些积分的完整列表以及其他相关功能的实现及其根源或派生,请参阅上scipy.special的在线文档,网址为 http://docs.scipy.org/doc/scipy- 0.13.0 / reference / special.html ,或在第 4 章,数值分析科学中,Francisco Blanco-Silva 的学习数值和科学计算科学[ 。
对于所有其他未包含在此函数列表中的函数,我们仍然需要可靠的解决方案来计算其根,导数或积分。 在本章中,我们将重点介绍允许执行最后两个操作的计算设备。
提示
下一章将介绍任何给定函数的根的计算(或*似)。
差异化
有三种方法可以计算导数:
-
数值微分是指某个点上给定函数的导数的*似过程。 在 SciPy 中,我们有以下过程,将详细介绍:
- 对于一般的单变量函数,中心差公式具有固定的间距。
- 始终可以通过柯西定理执行数值微分,将导数转换为定积分。 然后使用即将在后面一节中解释的数值积分技术来处理该积分。
-
符号区分是指函数派生函数的函数表达式的计算,几乎与我们手动进行的方式相同。 之所以称为符号,是因为符号与其数字对应物不同,它承担的是变量的作用,而不是数字或数字的向量。 要执行符号区分,我们需要一个计算机代数系统( CAS ),在 SciPy 堆栈中,这主要是通过
SymPy库实现的(请参见 http: //docs.sympy.org/latest/index.html )。 符号微分和后验评估是替代非常基本功能的数字微分的一个不错的选择。 但是,通常,此方法会导致代码过于复杂和效率低下。 尽管可能出现错误,但纯数字微分的速度还是首选的。 -
Automatic differentiation is another set of techniques to numerically evaluate the derivative of a function. It is not based upon any approximation schema. This is without a doubt the most powerful option in the context of high derivatives of multivariate functions.
注意
在 SciPy 堆栈中,这是通过不同的不相关库执行的。 一些最可靠的是
Theano( http://deeplearning.net/software/theano/ )或FuncDesigner( http://www.openopt.org/FuncDesigner )。 对于这些技术的全面描述和分析,可以在 http://alexey.radul.name/ideas/2013/introduction-to-automatic-differentiation/ 中找到很好的资源。
数值微分
数值微分的最基本方案是使用节点间距均匀的中心差分公式执行的。 为了保持对称性,需要奇数个节点以保证较小的舍入误差。 此简单算法的实现在模块scipy.misc中可用。
提示
有关模块scipy.misc以及其基本例程的枚举的信息,请参考位于的在线文档,网址为 http://docs.scipy.org/doc/scipy-0.13.0/reference/misc.html 。
为了*似多项式函数的一阶和二阶导数,例如, x = 1 时, f(x)= x 5 。 在距离dx=1e-6处有 15 个等距节点(以 x = 1 为中心)的情况下,我们可以发出以下命令:
In [1]: import numpy as np
In [2]: from scipy.misc import derivative
In [3]: def f(x): return x**5
In [4]: derivative(f, 1.0, dx=1e-6, order=15)
Out[4]: 4.9999999997262723
In [5]: derivative(f, 1.0, dx=1e-6, order=15, n=2)
Out[5]: 19.998683310705456
由于实际值分别为5和20,因此有些准确,但仍然令人失望。
提示
该方法的另一个缺陷(至少相对于 SciPy 中编码的实现而言)是这样的事实,即结果依赖于可能很大的和,而这些和是不稳定的。 作为用户,我们可以通过使用 Shewchuk 算法修改scipy.misc.derivative源中的循环来改善问题。
象征差异
多项式的精确微分可以通过模块numpy.polynomial实现:
In [6]: p = np.poly1d([1,0,0,0,0,0]); \
...: print p
5
1 x
In [7]: np.polyder(p,1)(1.0) In [7]: p.deriv()(1.0)
Out[7]: 5.0 Out[7]: 5.0
In [8]: np.polyder(p,2)(1.0) In [8]: p.deriv(2)(1.0)
Out[8]: 20.0 Out[8]: 20.0
符号差异是获得精确结果的另一种方法:
In [9]: from sympy import diff, symbols
In [10]: x = symbols('x', real=True)
In [11]: diff(x**5, x) In [12]: diff(x**5, x, x)
Out[11]: 5*x**4 Out[12]: 20*x**3
In [13]: diff(x**5, x).subs(x, 1.0)
Out[13]: 5.00000000000000
In [14]: diff(x**5, x, x).subs(x, 1.0)
Out[14]: 20.0000000000000
当我们区分比简单多项式更复杂的函数时,请注意略有改进(在符号表示和简化编码方面)。 例如,对于 g(x)= e -x sinx 在 x = 1 时:
In [15]: def g(x): return np.exp(-x) * np.sin(x)
In [16]: derivative(g, 1.0, dx=1e-6, order=101)
Out[16]: -0.11079376536871781
In [17]: from sympy import sin as Sin, exp as Exp
In [18]: diff(Exp(-x) * Sin(x), x).subs(x, 1.0)
Out[18]: -0.110793765306699
相对于其数值或自动对应项,符号微分的一个巨大优势是可以极其轻松地计算偏导数。 让我们通过计算多元函数 h(x,y,z)= e xyz 在 x = 1 处的四阶导数来说明这一点。 , y = 1 和 z = 2 :
In [19]: y, z = symbols('y z', real=True)
In [20]: diff(Exp(x * y * z), z, z, y, x).subs({x:1.0, y:1.0, z:2.0})
Out[20]: 133.003009780752
自动区分
第三种方法采用自动区分。 对于此示例,我们将使用库FuncDesigner:
In [21]: from FuncDesigner import oovar, exp as EXP, sin as SIN
In [22]: X = oovar('X'); \
....: G = EXP(-X) * SIN(X)
In [23]: G.D({X: 1.0}, X)
Out[23]: -0.11079376530669924
结果显然比通过数值微分获得的结果更准确。 另外,不需要提供任何额外的参数。
积分
为了在合适的域上实现功能的确定积分,我们主要有两种方法-数值积分和符号 积分。
数值积分是指通过正交过程*似定积分。 根据给出函数 f(x)的方式,积分的域,其奇异性的知识以及正交的选择,我们可以采用不同的方法来解决此问题:
- 对于单变量多项式,在每个有限区间以代数形式实现精确积分
- 对于在其范围内作为有限样本集合给出的函数:
- 复合梯形法则
- 辛普森梯形法则
- Romberg 整合方案
- 对于以有限间隔作为 Python 函数给出的通用单变量函数:
- 固定阶高斯正交
- 固定公差高斯正交
- 通过应用 21 点,43 点和 87 点的高斯-克朗定律,实现简单的非自适应正交
- 简单的自适应正交,在每个子间隔上进行细分和正交
- 基于每个子间隔内 21 点高斯-克朗德积分的盲全局自适应正交,并具有加速过程(彼得·韦恩的 epsilon 算法):
- 基于先前的全局自适应正交,但具有用户提供的奇异点/间断点位置
- 自适应 Romberg 集成方案
- 对于作为无限制区间上的 Python 函数给出的单变量函数,存在全局自适应正交。 该过程将无限间隔转换为半开放间隔,并在每个子间隔内应用 15 点高斯-克朗德积分。
- 对于在类型 I 域上将作为 Python 函数给出的多元函数(将在稍后进行描述),通常使用在每个维度上迭代应用自适应单变量正交的方法。
在许多情况下,借助符号计算,甚至对于无界域也可以执行精确的积分。 在 SciPy 堆栈中,为此,我们为基本函数提供了 Risch 算法的实现,为非基本积分提供了 Meijer G 函数。 两种方法都位于SymPy库中。 不幸的是,这些符号过程不适用于所有功能。 而且,由于所生成代码的复杂性,通常,用这种方法获得的解决不像任何数值*似那样快。
符号整合
可以通过微积分的基本定理,通过模块numpy.polynomial,非常精确地计算有限域[a,b]上多项式函数的定积分。 例如,要计算区间[-1,1]上多项式 p(x)= x 5 的积分,我们可以发出:
In [1]: import numpy as np
In [2]: p = np.poly1d([1,0,0,0,0,0]); \
...: print p; \
...: print p.integ()
5
1 x
6
0.1667 x
In [3]: p.integ()(1.0) - p.integ()(-1.0)
Out[3]: 0.0
通常,难以获得泛型函数的确定积分的精确值,并且计算效率低下。 在某些情况下,借助于 Risch 算法(对于基本函数)和 Meijer G 函数(对于非基本积分),可以通过符号积分来实现。 可以使用集成在库SymPy中的通用例程来调用这两种方法。 该例程足够聪明,可以根据源函数来决定使用哪种算法。
让我们向您展示一些示例,这些示例从前一种情况的多项式的定积分开始:
In [4]: from sympy import integrate, symbols
In [5]: x, y = symbols('x y', real=True)
In [6]: integrate(x**5, x)
Out[6]: x**6/6
In [7]: integrate(x**5, (x, -1, 1))
Out[7]: 0
让我们尝试一些更复杂的事情。 函数 f(x)= e -x sinx 的定积分在区间[0,1]上:
In [8]: from sympy import N, exp as Exp, sin as Sin
In [9]: integrate(Exp(-x) * Sin(x), x)
Out[9]: -exp(-x)*sin(x)/2 - exp(-x)*cos(x)/2
In [10]: integrate(Exp(-x) * Sin(x), (x, 0, 1))
Out[10]: -exp(-1)*sin(1)/2 - exp(-1)*cos(1)/2 + 1/2
In [11]: N(_)
Out[11]: 0.245837007000237
符号集成在工作时会以正确的方式对待奇点:
In [12]: integrate(Sin(x) / x, x)
Out[12]: Si(x)
In [13]: integrate(Sin(x) / x, (x, 0, 1))
Out[13]: Si(1)
In [14]: N(_)
Out[14]: 0.946083070367183
In [15]: integrate(x**(1/x), (x, 0, 1))
Out[15]: 1/2
也可以在无界域上进行集成:
In [16]: from sympy import oo
In [17]: integrate(Exp(-x**2), (x,0,+oo))
Out[17]: sqrt(pi)/2
甚至可以执行多变量集成:
In [18]: integrate(Exp(-x**2-y**2), (x, -oo, +oo), (y, -oo, +oo))
Out[18]: pi
但是,我们需要特别强调这一点-符号集成在简单情况下效率不高(并且可能不起作用!),如以下示例所示:
In [19]: integrate(Sin(x)**Sin(x), x)
Integral(sin(x)**sin(x), x)
In [20]: integrate(Sin(x)**Sin(x), (x, 0, 1))
Integral(sin(x)**sin(x), (x, 0, 1))
即使适用于简单情况,它也会生成复杂的代码,并且可能会使用过多的计算资源。
数值积分
解决这些问题的最佳方法是在数值积分的帮助下获得良好的*似值。 根据功能的类型和集成域,有不同的技术。 让我们详细研究它们。
在有限间隔上没有奇异的函数
数值积分中的基本问题是在有限间隔[a,b]上*似任何函数 f(x)的定积分。 通常,如果函数 f(x)不具有奇异性或不连续性,我们可以通过将不同的插值与分段多项式进行积分来获得简单的正交公式(因为对它们进行了精确评估):
- 通过集成分段线性插值器(每两个连续的节点)来实现复合梯形法则
- 辛普森法则是通过集成分段多项式插值器来实现的,其中每两个连续的子间隔我们拟合一个抛物线
- 在前一种情况下,如果我们进一步施加 Hermite 插值,则可以获得复合 Simpson 规则
我们分别通过例程cumtrapz和simps在模块scipy.integrate中提供了用于复合梯形规则和复合 Simpson 规则的高效算法。 让我们向您展示如何将这些简单的正交公式用于多项式示例:
In [21]: from scipy.integrate import cumtrapz, simps
In [22]: def f(x): return x**5
In [23]: nodes = np.linspace(-1, 1, 100)
In [24]: simps(f(nodes), nodes)
Out[24]: -1.3877787807814457e-17
In [25]: cumtrapz(f(nodes), nodes)
Out[25]:
array([ -1.92221161e-02, -3.65619927e-02, -5.21700680e-02,
-6.61875756e-02, -7.87469280e-02, -8.99720915e-02,
-9.99789539e-02, -1.08875683e-01, -1.16763077e-01,
-1.23734908e-01, -1.29878257e-01, -1.35273836e-01,
-1.39996314e-01, -1.44114617e-01, -1.47692240e-01,
-1.50787532e-01, -1.53453988e-01, -1.55740523e-01,
-1.57691741e-01, -1.59348197e-01, -1.60746651e-01,
-1.61920310e-01, -1.62899066e-01, -1.63709727e-01,
-1.64376231e-01, -1.64919865e-01, -1.65359463e-01,
-1.65711607e-01, -1.65990811e-01, -1.66209700e-01,
-1.66379187e-01, -1.66508627e-01, -1.66605982e-01,
-1.66677959e-01, -1.66730153e-01, -1.66767180e-01,
-1.66792794e-01, -1.66810003e-01, -1.66821177e-01,
-1.66828145e-01, -1.66832283e-01, -1.66834598e-01,
-1.66835799e-01, -1.66836364e-01, -1.66836598e-01,
-1.66836678e-01, -1.66836700e-01, -1.66836703e-01,
-1.66836703e-01, -1.66836703e-01, -1.66836703e-01,
-1.66836700e-01, -1.66836678e-01, -1.66836598e-01,
-1.66836364e-01, -1.66835799e-01, -1.66834598e-01,
-1.66832283e-01, -1.66828145e-01, -1.66821177e-01,
-1.66810003e-01, -1.66792794e-01, -1.66767180e-01,
-1.66730153e-01, -1.66677959e-01, -1.66605982e-01,
-1.66508627e-01, -1.66379187e-01, -1.66209700e-01,
-1.65990811e-01, -1.65711607e-01, -1.65359463e-01,
-1.64919865e-01, -1.64376231e-01, -1.63709727e-01,
-1.62899066e-01, -1.61920310e-01, -1.60746651e-01,
-1.59348197e-01, -1.57691741e-01, -1.55740523e-01,
-1.53453988e-01, -1.50787532e-01, -1.47692240e-01,
-1.44114617e-01, -1.39996314e-01, -1.35273836e-01,
-1.29878257e-01, -1.23734908e-01, -1.16763077e-01,
-1.08875683e-01, -9.99789539e-02, -8.99720915e-02,
-7.87469280e-02, -6.61875756e-02, -5.21700680e-02,
-3.65619927e-02, -1.92221161e-02, -1.73472348e-17])
提示
例程cumtrapz计算指定子间隔内的累积积分。 因此,输出的最后一项是我们寻求的正交值。 当然,我们可以通过简单地访问该条目来仅报告所需的积分:
In [26]: cumtrapz(f(nodes), nodes)[-1]
Out[26]: -1.7347234759768071e-17
这两种算法的实现未明确计算插值器。 最终公式是这里的目标,并且它在 SciPy 中的编码方式是通过 Newton-Cotes 求积法。
执行 Newton-Cotes 的例程是隐藏的(在某种意义上说,它们未在 SciPy 的官方页面上的教程或文档中进行报告),并且仅供cumtrapz或simps内部使用。 它们仅提供相应的系数,这些系数会乘以节点处的功能评估。
但是,在正确的情况下,牛顿-科特斯正交公式通常通常非常准确。 在许多情况下,它们可以用于计算更好的*似值,而不必遵守梯形或辛普森规则。
让我们展示一下它如何在我们的运行示例中工作,现在在间隔[-1,1]中只有四个等距节点:
In [27]: from scipy.integrate import newton_cotes
In [28]: coefficients, abs_error = newton_cotes(3, equal=True); \
....: nodes = np.linspace(-1, 1, 4); \
....: print coefficients
[ 0.375 1.125 1.125 0.375]
In [29]: integral = (coefficients * f(nodes)).sum(); \
....: print integral
0.0
In [30]: from math import fsum
In [31]: integral = fsum(coefficients * f(nodes)); \
....: print integral
-7.8062556419e-18
如果我们选择的节点之间的间距相等,则在特殊情况下,如果子间隔的数量为 2 的幂,则梯形规则将得到改进。 在这种情况下,我们可以使用 Romberg 规则-使用 Richardson 外推法的一种改进。 我们可以在同一模块中使用例程romb访问它。
让我们将结果与正在运行的示例进行比较,这次使用间隔[-1,1]中的大小为1/32的 64 个子间隔:
In [32]: from scipy.integrate import romb
In [33]: nodes = np.linspace(-1, 1, 65)
In [34]: romb(f(nodes), dx=1./32)
0.0
我们可以选择报告表,该表显示了从给定节点进行的 Richardson 外推:
In [35]: romb(f(nodes), dx=1./32, show=True)
Richardson Extrapolation Table for Romberg Integration
====================================================================
0.00000
0.00000 0.00000
0.00000 0.00000 0.00000
0.00000 0.00000 0.00000 0.00000
0.00000 0.00000 0.00000 0.00000 0.00000
0.00000 0.00000 0.00000 0.00000 0.00000 0.00000
====================================================================
Out[35]: 0.0
对于节点的选择,我们可能没有任何偏好,但是对于我们的数值积分方案,使用 Romberg 规则仍然有我们的想法。 在这种情况下,我们可以使用例程romberg,对于该例程,我们只需要提供函数的表达式和积分的限制即可。 (可选)我们可以提供误差的绝对或相对公差(默认均设置为1.48e-8):
In [36]: from scipy.integrate import romberg
In [37]: romberg(f, -1, 1, show=True)
Romberg integration of <function vfunc at 0x10ffa8c08> from [-1, 1]
Steps StepSize Results
1 2.000000 0.000000
2 1.000000 0.000000 0.000000
The final result is 0.0 after 3 function evaluations.
Out[37]: 0.0
另一种可能性是使用高斯正交公式。 由于通过在内部计算节点的最佳可能选择来获得*似值的精度,因此这些功能更为强大。 模块scipy.integrate中有两个基本例程执行该算法的实现:quadrature(如果我们要指定容差),或fixed_quad(如果我们要指定节点数(而不是它们的位置!))。 :
In [38]: from scipy.integrate import quadrature, fixed_quad
In [39]: value, absolute_error = quadrature(f, -1, 1, tol=1.49e-8); \
....: print value
0.0
In [40]: value, absolute_error = fixed_quad(f, -1, 1, n=4); \
....: print value # four nodes
-9.45424294407e-16
通过模块scipy.integrate中的函数quad获得使用自适应方案执行高斯正交的更高级的方法。 此函数是 Fortran 库QUADPACK中例程QAGS的包装。 该算法将积分域划分为几个子间隔,并在每个子间隔上执行 21 点高斯-克朗罗德正交规则。 借助 Peter Wynn 的 epsilon 算法,可以实现进一步的加速。
提示
有关QAGS以及QUADPACK库中其他例程的更多信息,请参考 netlib 存储库: http://www.netlib.org/quadpack/ 。
让我们将其与正在运行的示例进行比较:
In [41]: from scipy.integrate import quad
In [42]: value, absolute_error = quad(f, -1, 1); \
....: print value
0.0
我们可以通过将可选参数full_output设置为True来获得实现细节。 这为我们提供了额外的 Python 字典,其中包含有用的信息:
In [43]: value, abs_error, info = quad(f, -1, 1, full_output=True)
In [44]: info.keys()
Out[44]: ['rlist', 'last', 'elist', 'iord', 'alist', 'blist',
'neval']
In [45]: print "{0} function evaluations".format(info['neval'])
21 function evaluations
In [46]: print "Used {0} subintervals".format(info['last'])
Used 1 subintervals
为了完全理解info的所有不同输出,我们需要了解计算高斯正交的基础算法。 这些特殊例程使用基于 Chebyshev 矩的 Clensaw-Curtis 方法。
在前面的示例中,默认情况下,该代码尝试使用 50 个 Chebyshev 矩。 由于被整数的简单性,并且由于仅需要一个子间隔,因此仅需使用这些矩之一。 当我们从字典信息中报告 50 个条目的一维输出rlist,elist,alist和blist时,我们可以不理会每个条目的最后 49 个条目提供的信息:
In [47]: np.set_printoptions(precision=2, suppress=True)
In [48]: print info['rlist'] # integral approx on subintervals
[ 0.00e+000 2.32e+077 6.93e-310 0.00e+000 0.00e+000
0.00e+000 0.00e+000 0.00e+000 0.00e+000 0.00e+000
0.00e+000 0.00e+000 0.00e+000 0.00e+000 0.00e+000
0.00e+000 0.00e+000 0.00e+000 0.00e+000 0.00e+000
0.00e+000 6.45e-314 2.19e-314 6.93e-310 0.00e+000
0.00e+000 0.00e+000 0.00e+000 0.00e+000 0.00e+000
0.00e+000 0.00e+000 0.00e+000 0.00e+000 0.00e+000
0.00e+000 0.00e+000 0.00e+000 0.00e+000 0.00e+000
0.00e+000 0.00e+000 -1.48e-224 2.19e-314 6.93e-310
0.00e+000 0.00e+000 0.00e+000 0.00e+000 0.00e+000]
In [49]: print info['elist'] # abs error estimates on subintervals
[ 3.70e-015 2.32e+077 3.41e-322 0.00e+000 0.00e+000
0.00e+000 0.00e+000 0.00e+000 0.00e+000 0.00e+000
0.00e+000 0.00e+000 0.00e+000 0.00e+000 0.00e+000
0.00e+000 0.00e+000 0.00e+000 0.00e+000 7.30e+245
2.19e-314 6.93e-310 0.00e+000 0.00e+000 0.00e+000
4.74e+246 2.20e-314 6.93e-310 0.00e+000 0.00e+000
0.00e+000 0.00e+000 0.00e+000 0.00e+000 -9.52e+207
2.19e-314 6.93e-310 0.00e+000 0.00e+000 0.00e+000
0.00e+000 0.00e+000 0.00e+000 0.00e+000 0.00e+000
0.00e+000 2.00e+000 2.00e+000 2.27e-322 1.05e-319]
In [50]: print info['alist'] # subintervals left end points
[-1\. 2\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\.
0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\.
0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\.
0\. 0\. 0\. 0\. 0.]
In [51]: print info['blist'] # subintervals right end pts
[ 1\. 2\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\.
0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\.
0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\.
0\. 0\. 0\. 2\. -0.]
可以施加不同数量的切比雪夫矩。 我们使用可选参数maxp1来执行此操作,该参数对该数字强加一个上限(而不是固定该数字,以获得最佳结果)。
即使 f(x)光滑,形式为 f(x)cos(wx)或 f(x)sin(wx)的振荡积分, 尤其棘手。 积分器quad可以通过调用QUADPACK中的例程QAWO处理这些积分。 我们可以通过使用wvar=w指定参数weight='cos'或weight='sin'来采用此方法。
例如,对于区间[-10,10]上 g(x)= sin(x)e x 的积分,我们将此方法与基本 quad。 我们可以执行以下操作:
In [52]: def f(x): return np.sin(x) * np.exp(x)
In [53]: g = np.exp
In [54]: quad(g, -10, 10, weight='sin', wvar=1)
Out[54]: (3249.4589405744427, 5.365398805302381e-08)
In [55]: quad(f, -10, 10)
Out[55]: (3249.458940574436, 1.1767634585879705e-05)
注意绝对误差的显着增加。
提示
有关本节中探讨的所有正交公式的详细信息和理论,请参考第 3 章,**的数值微分和积分* Walter Gautchi 的数值分析[* 。
有界域上具有奇异性的函数
积分的第二种情况是具有奇异性的函数在有限区间[a,b]上的确定积分。 我们考虑两种情况:加权函数和具有奇点的泛型函数。
加权函数
加权函数可以实现为 f(x)w(x)类型的乘积,用于某些*滑函数 f(x)和非负加权函数 w( x)包含奇点。 cos(πx/ 2)/ √ x 给出了一个说明性示例。 我们可以将这种情况视为 cos(πx/ 2)与 w(x)= 1 / √ x 的乘积。 权重在 x = 0 处呈现单个奇点,否则*滑。
处理这些积分的常用方法是借助加权高斯正交公式。 例如,要执行 f (x)/(x-c)形式的函数的主值积分,我们向quad发出参数weight='cauchy'和wvar=c。 这从QUADPACK调用例程QAWC。
让我们在间隔[-1,1]上试验 g(x)= sin(x)/ x 的菲涅耳型正弦积分,并将其与romberg进行比较:
In [56]: value, abs_error = quad(f, -1, 1, weight='cauchy',wvar=0); \
....: print value
1.89216614073
In [57]: romberg(g, -1, 1)
Out[57]: 2.35040238729
在具有权重的函数积分的情况下(xa) α (bx) β ,其中a和b是整合域的端点,并且alpha和beta都大于-1,我们向quad发出了参数weight='alg'和wvar=(alpha, beta)。 这从QUADPACK调用例程QAWS。
让我们用 g(x)= cos(πx/ 2)/ √ x 的菲涅耳型余弦积分进行实验,并进行比较 与quadrature一起使用:
In [58]: def f(x): return np.cos(np.pi * x * 0.5)
In [59]: def g(x): return np.cos(np.pi * x * 0.5) / np.sqrt(x)
In [60]: value, abs_error = quad(f, 0, 1, weight='alg', \
wvar=(-0.5,0)); \
....: print value
1.55978680075
In [61]: quadrature(g, 0, 1)
quadrature.py:178: AccuracyWarning: maxiter (50) exceeded. Latest
difference = 3.483182e-04
AccuracyWarning)
Out[61]: (1.5425452942607543, 0.00034831815190772275)
如果权重的形式为 w(x)=(xa) α (bx) β[ ln(xa), w(x)=(xa) α (bx) β ln(bx)或 w(x)=(xa) α (bx) β ln(xa)ln(bx),我们用以下参数发布quad: 分别为weight='alg-loga'或weight='alg-logb'或weight='alg-log',并分别为wvar=(alpha, beta)。 例如,对于间隔[0,1]上的函数 g(x)= x 2 ln(x) 发出以下命令:
In [62]: def f(x): return x**2
In [63]: def g(x): return x**2 * np.log(x)
In [64]: value, abs_error = quad(f, 0, 1, weight='alg-loga', \
wvar=(0,0)); \
....: print value
-0.111111111111
该积分的实际值为-1/9。
具有奇异性的一般功能
通常,我们可能会以奇异的形式处理函数,这些奇数不符合上一节中指出的漂亮形式 f(x)w(x)。 在这种情况下,如果我们知道奇点的位置,则可以向带有可选参数点的积分器四边形表示。 积分器quad调用QUADPACK中的例程QAGP。 例如,对于函数 g(x)= floor(x)ln(x),它在每个整数上观察到一个奇数,以对区间[1,8]进行积分,我们可以发出以下命令:
In [65]: def g(x): return np.floor(x) * np.log(x)
In [66]: quad(g, 1, 8, points=np.arange(8)+1)
Out[66]: (45.802300241541005, 5.085076830895319e-13)
将此与简单的四边形计算进行比较,而无需指出任何奇点,如下面的代码行所示:
In [67]: quad(g, 1, 8)
quadpack.py:295: UserWarning: The maximum number of subdivisions (50)
has been achieved.
If increasing the limit yields no improvement it is advised to
analyze the integrand in order to determine the difficulties. If the
position of a local difficulty can be determined (singularity,
discontinuity) one will probably gain from splitting up the interval
and calling the integrator on the subranges. Perhaps a special-
purpose integrator should be used.
warnings.warn(msg)
Out[67]: (45.80231242134967, 8.09803045669355e-05)
在无界域上集成
通过调用QUADPACK中的例程QAGI,通用积分器quad还能够使用自适应正交公式在无界域上计算定积分。 此过程不适用于'cauchy'或任何'alg'型砝码选项。
通常,如果要集成的函数不具有奇异性,则*似值是可靠的。 奇异性的存在给出不可靠的积分,如以下示例所示:
In [68]: def f(x): return 2 * np.exp(-x**2) / np.sqrt(np.pi)
In [69]: value, absolute_error = quad(f, 0, np.inf); \
....: print value
1.0
In [70]: def f(x): return np.sin(x)/x
In [71]: integrate(Sin(x)/x, (x, 0, oo))
Out[71]: pi/2
In [72]: value, absolute_error = quad(f, 0, np.inf); \
....: print value # ouch!
2.24786796347
对于无界域中的振荡积分,除了发出带有参数weight='cos'或weight='sin'以及相应的wvar参数的quad之外,我们还可以为内部使用的循环数设置上限。 为此,我们将可选参数limlst设置为所需的界限。 通常最好将其设置为大于三的值。 例如,对于 [1,∞] 上sinc函数的傅里叶积分,我们可以发出以下命令:
In [73]: def f(x): return 1./x
In [74]: quad(f, 1, np.inf weight='sin', wvar=1, limlst=5)
quadpack.py:295: UserWarning: The maximum number of cycles allowed
has been achieved., e.e. of subintervals (a+(k-1)c, a+kc) where c =
(2*int(abs(omega)+1))*pi/abs(omega), for k = 1, 2, ..., lst. One can
allow more cycles by increasing the value of limlst. Look at
info['ierlst'] with full_output=1.
warnings.warn(msg)
Out[74]: (0.636293340511029, 1.3041427865109276)
In [75]: quad(f, 1, np.inf, weight='sin', wvar=1, limlst=50)
Out[75]: (0.6247132564795975, 1.4220476353655983e-08)
数值多元积分
通过应用自适应高斯正交规则,还可以在不同的域上执行多元数值积分。 为此,在模块scipy.integrate中,我们必须执行例程dblquad(双积分),tplquad(三重积分)和nquad(多个变量的积分)。
这些例程只能在 I 型区域上计算定积分:
- 在两个维度中,类型 I 域可以以 {(x,y)的形式写:a < x < b,f(x)< y < h(x)} 代表两个数字
a和b和两个单变量函数 f(x)和 h(x)。 - 在三个维度上,类型 I 区域可以以 {(x,y,z)的形式写:a < x < b,f(x)< y < h(x ),q(x,y)< z < r(x,y)} 对于数字
a,b,单变量函数 f(x), h(x)和二元函数 q(x,y),r(x,y)。 - 在三个以上的维度中,I 型区域可以按与其双重和三重对应区域相似的方式顺序写入。 第一个变量以两个数字为界。 第二个变量受第一个变量的两个单变量函数限制。 第三个变量受两个第一个变量的两个双变量函数限制,依此类推。
让我们对In [18]行中的示例函数进行数值积分。 请注意在要集成的函数的定义中必须引入不同变量的顺序:
In [76]: def f(x, y): return np.exp(-x**2 - y**2)
In [77]: from scipy.integrate import dblquad
In [78]: dblquad(f, 0, np.inf, lambda x:0, lambda x:np.inf)
Out[78]: (0.785398163397, 6.29467149642e-09)
摘要
在本章中,我们已经掌握了计算功能差异和集成的所有不同方法。 我们了解到scipy库具有非常健壮的例程,可以用数值方式计算所有这些操作的*似值(必要时包装有效的 Fortran 库)。 我们还了解到,可以访问 SciPy 堆栈中的其他库以符号或自动方式执行操作。
在下一章中,我们将探讨在非线性函数的背景下求解方程或方程组的理论和方法,以及为优化目的而计算极值。
四、非线性方程式和最优化
在本章中,我们将回顾对数值数学的发展至关重要的两个基本运算:零值搜索和实值函数的极值。
动机
让我们从第 2 章,“插值和*似”中重新审视 Runge 的示例,在该示例中,我们使用了从-5到5的间隔中的 11 个等距节点计算了 Runge 函数的 Lagrange 插值:
In [1]: import numpy as np, matplotlib.pyplot as plt; \
...: from scipy.interpolate import BarycentricInterpolator
In [2]: def f(t): return 1\. / (1\. + t**2)
In [3]: nodes = np.linspace(-5, 5, 11); \
...: domain = np.linspace(-5, 5, 128); \
...: interpolant = BarycentricInterpolator(nodes, f(nodes))
In [4]: plt.figure(); \
...: plt.subplot(121); \
...: plt.plot(domain, f(domain), 'r-', label='original'); \
...: plt.plot(nodes, f(nodes), 'ro', label='nodes'); \
...: plt.plot(domain, interpolant1(domain), 'b--',
...: label='interpolant'); \
...: plt.legend(loc=9); \
...: plt.subplot(122); \
...: plt.plot(domain, np.abs(f(domain)-interpolant1(domain))); \
...: plt.title('error or interpolation'); \
...: plt.show()

衡量此方案成功与否的一种方法是通过计算原始函数与插值之间差异的统一范数。 在这种情况下,该标准接* 2.0。 我们可以通过对域中的大量点执行以下计算来*似此值:
In [5]: error1a = np.abs(f(domain)-interpolant(domain)).max(); \
...: print error1a
1.91232007608
但是,这是实际误差的粗略*似值。 为了计算真实的范数,我们需要一种机制来在有限的时间间隔内而不是在离散的点集上计算函数的实际最大值。 对于当前示例,要执行此操作,我们将使用模块scipy.optimize中的例程minimize_scalar。
让我们以两种不同的方式解决此问题,以说明优化算法的一个可能的陷阱:
- 在第一种情况下,我们将利用问题的对称性(
f和interpolator均为偶函数),并提取从0到5的区间的差范数的最大值 - 在第二种情况下,我们在
-5至5的整个时间间隔内执行相同的操作。
我们将在计算之后得出结论:
In [6]: from scipy.optimize import minimize_scalar
In [7]: def uniform_norm(func, a, b):
...: g = lambda t: -np.abs(func(t))
...: output = minimize_scalar(g, method="bounded",
...: bounds=(a, b))
...: return -output.fun
...:
In [8]: def difference(t): return f(t) - interpolant(t)
In [9]: error1b = uniform_norm(difference, 0., 5.)
...: print error1b
1.9156589182259303
In [10]: error1c = uniform_norm(difference, -5., 5.); \
....: print error1c
0.32761452331581842
提示
刚刚发生了什么? 例程minimize_scalar使用一种迭代算法,该算法由于问题的对称性而感到困惑,并收敛到一个局部最大值,而不是所请求的全局最大值。
第一个示例说明了本章的主题之一(及其危害):实值函数的约束极值的计算。
*似显然不是很好。 切比雪夫(Chebyshev)的一个定理指出,最佳的多项式*似是通过明智地选择节点来实现的-恰比雪夫(Chebyshev)多项式的零点正好! 我们可以使用模块scipy.special中的例程t_roots来收集所有这些根。 在我们的运行示例中,最佳 11 个节点的选择将基于 11 度 Chebyshev 多项式的根,并在插值的间隔内正确转换:
In [11]: from scipy.special import t_roots
In [12]: nodes = 5 * t_roots(11)[0]; \
....: print nodes
[ -4.94910721e+00 -4.54815998e+00 -3.77874787e+00 -2.70320409e+00
-1.40866278e+00 -1.34623782e-15 1.40866278e+00 2.70320409e+00
3.77874787e+00 4.54815998e+00 4.94910721e+00]
In [13]: interpolant = BarycentricInterpolator(nodes, f(nodes))
In [14]: plt.figure(); \
....: plt.subplot(121); \
....: plt.plot(domain, f(domain), 'r-', label='original'); \
....: plt.plot(nodes, f(nodes), 'ro', label='nodes'); \
....: plt.plot(domain, interpolant(domain), 'b--',
....: label='interpolant')); \
....: plt.subplot(122); \
....: plt.plot(domain, np.abs(f(domain)-interpolant(domain))); \
....: plt.title('error or interpolation'); \
....: plt.show()

这是插值器质量的显着提高。 多亏了我们将位置合理的节点作为多项式的根进行了计算。 让我们计算该插值的统一范数:
In [15]: def difference(t): return f(t) - interpolant(t)
In [16]: error2 = uniform_norm(difference, 0., 2.)
....: print error2
0.10915351095
对于某些经常出现的情况,例如 Chebyshev 多项式零点的示例,模块scipy.special具有例程,可以按规定的精度收集这些值。 有关这些特殊情况的完整列表,请参考上的scipy.special在线文档,网址为 http://docs.scipy.org/doc/scipy/reference/special.html 。
对于一般情况,我们希望有一套扎根的技术。 这正是本章讨论的另一个主题。
非线性方程和系统
在线性方程和系统的解中, f(x)= 0 ,我们可以选择使用直接方法或迭代过程。 这种设置中的一种直接方法就是简单地应用仅涉及四个基本运算的精确公式:加,减,乘和除。 当取消发生时,主要是每当存在和减时,就会出现这种方法的问题。 迭代方法不是在有限数量的运算中计算解决方案,而是计算与所述解决方案的越来越*的*似值,从而提高了每个步骤的准确性。
对于非线性方程,直接方法很少是一个好主意。 即使公式可用,非基本运算的存在也会导致舍入错误。 让我们用一个非常基本的例子来看看。
考虑二次方程 ax 2 + bx + c = 0 ,其中 a = 10 –10 , b = –(10 10 + 1)/ 10 10 和 c = 1 。 这些是多项式 p(x)= 10 –10 (x-1)(x–10 [ 10 ),根 x = 1 和 x = 10 10 。 请注意以下命令中二次方的行为:
In [1]: import numpy as np
In [2]: a, b, c = 1.0e-10, -(1.0e10 + 1.)/1.0e10, 1.
In [3]: (-b - np.sqrt(b**2 - 4*a*c))/(2*a)
Out[3]: 1.00000000082740371
由于取消引起的一个明显的舍入误差已经扩散。 在这种情况下,可以通过将该公式的分子和分母乘以其分母的共轭数,并改用所得公式来解决这种情况:
In [4]: 2*c / (-b + np.sqrt(b**2 - 4*a*c))
Out[4]: 1.0
甚至sympy库中编码的代数求解器也存在此缺陷,如以下示例所示:
提示
sympy库具有一组代数求解器,并且都可以从通用例程solve中访问它们。 当前,该方法求解单变量多项式,先验方程以及它们的分段组合。 它还解决了线性和多项式方程组。
有关更多信息,请参考的sympy官方文档,网址为 http://docs.sympy.org/dev/modules/solvers/solvers.html 。
In [5]: from sympy import symbols, solve
In [6]: x = symbols('x', real=True)
In [7]: solve(a*x**2 + b*x + c)
Out[7]: [1.00000000000000, 9999999999.00000]
为了避免对我们的解决方案的准确性进行猜测或微调每个可以解决非线性方程式的公式,我们始终可以采用迭代过程来任意*似地*似。
单变量函数的迭代方法
标量函数的迭代过程可以分为三类:
- 括弧方法,其中算法跟踪包含根的间隔的端点。 我们有以下算法:
- 二等分法
- Regula falsi(错误位置方法)
- 正割方法,具有以下算法:
- 割线法
- 牛顿-拉夫森法
- 插值法
- 逆插值法
- 定点迭代法
- 布伦特方法,它是对分,割线和逆插值方法的组合。
现在,让我们探索 SciPy 堆栈中包含的方法。
包围方式
最基本的算法是*分法-给定区间 [a,b] 满足 f(a)f(b)的连续函数 f(x) < 0 。 该方法通过*分间隔并保留存在解决方案的子间隔来构造一个*似序列。 这是一个缓慢的过程(线性收敛),但是它永远不会收敛到一个解决方案。 在模块scipy.optimize中,我们有一个实现,例程bisect。
让我们首先通过运行示例来探索这种方法。 由于 p(0)和 p(2)的符号不同,因此在 [0 ,2]区间中必须有根 :
In [8]: from scipy.optimize import bisect
In [9]: p = np.poly1d([a,b,c])
In [10]: bisect(p, 0, 2)
Out[10]: 1.0
提示
请注意,我们选择用numpy.poly1d类表示 p(x)。 每当我们需要使用多项式时,在 SciPy 中处理它们的最佳方法就是通过此类。 这样可以确保使用 Horner 方案评估多项式,该方案提供的计算速度比任何其他 Lambda 表示都要快。
但是,对于具有很高阶数的多项式,由于抵消引起的舍入误差,霍纳方案可能不准确。 在这种情况下,必须小心。
二等分方法的一个问题是,它对初始端点的选择非常敏感,但通常,可以通过请求适当的公差来提高计算解的质量,如以下示例所示:
In [11]: bisect(p, -1, 2)
Out[11]: 1.0000000000002274
In [12]: bisect(p, -1, 2, xtol=1e-15)
Out[12]: 0.9999999999999996
In [13]: bisect(p, -1, 2, xtol=1e-15, rtol=1e-15)
Out[13]: 1.0000000000000009
更先进的技术集基于法规。 给定间隔 [a,b] 包含函数 f(x)的根,计算通过点(a,f(a) )和(b,f(b))。 这条线与 [a,b] 内的 x 轴相交。 我们将这一点用于下一个包围步骤。 在模块scipy.optimize中,我们有例程ridder(基于 C.Ridders 开发的算法对法规进行的改进),它呈现二次收敛性。
为了说明任何求解器之间的行为差异,我们可以使用每种算法的可选输出RootResult,如以下会话所示:
In [14]: soln, info = bisect(p, -1, 2, full_output=True)
In [15]: print "Iterations: {0}".format(info.iterations)
Iterations: 42
In [16]: print "Function calls: {0}".format(info.function_calls)
Function calls: 44
In [17]: from scipy.optimize import ridder
In [18]: soln, info = ridder(p, -1, 2, full_output=True)
In [19]: print "Solution: {0}".format(info.root)
Solution: 1.0
In [20]: print "Iterations: {0}".format(info.iterations)
Iterations: 1
In [21]: print "Function calls: {0}".format(info.function_calls)
Function calls: 4
正割方法
下一步技术是基于割线方法及其极限情况。 正割方法在计算上非常类似于法规。 而不是用括号括起来,我们从任何两个初始猜测开始 x 0 , x 1 ,并通过(x 0 计算线的交点 x 2 ] ,f(x 0 ))和(x 1 ,f(x 1 ))。 下一步对猜测重复执行相同的操作 x 1 , x 2 以计算新的*似值 x 3 ,然后重复该过程,直到获得对根的满意*似值为止。
通过使用比割线更智能的选择来搜索与 x 轴的交点,可以获得对该方法的改进。 Newton-Raphson 方法使用 f(x)的一阶导数来计算更好的相交线。 Halley 方法使用 f(x)的一阶和二阶导数来计算抛物线弧与 x 轴的交点。
割线方法的收敛阶数约为 1.61803,而牛顿-拉夫森方程是二次方,哈雷方程是三次方。
对于标量函数,可以使用模块scipy.optimize中的通用例程newton访问所有三种方法(割线,牛顿,哈雷)。 例程的必需参数是函数 f(x),以及初始猜测 x 0 。
让我们处理一个涉及方程 sin(x) / x = 0 的更复杂的示例:
In [22]: from scipy.optimize import newton; \
....: from sympy import sin as Sin, pi, diff, lambdify
In [23]: def f(t): return np.sin(t)/t
In [24]: f0 = Sin(x)/x
In [25]: f1prime = lambdify(x, diff(f0, x), "numpy"); \
....: f2prime = lambdify(x, diff(f0, x, 2), "numpy")
In [26]: solve(f0, x)
Out[26]: [pi]
In [27]: newton(f, 1) # pure secant
Out[27]: 3.1415926535897931
In [28]: newton(f, 1, fprime=f1prime) # Newton-Raphson
Out[28]: 3.1415926535897931
In [29]: newton(f, 1, fprime=f1prime, fprime2=f2prime) # Halley
Out[29]: 3.1415926535897931
这三种方法中的任何一种的问题都是不能始终保证收敛。 例程newton具有一种机制,可以防止算法执行某些步骤,并且在发生这种情况时,会引发运行时错误,从而将这种情况告知我们。 牛顿-拉夫森法和哈雷法中不良行为的一个典型例子是方程 x 20 = 1 如果我们最初的猜测恰好是 x = 0.5 ,则其根的根数 x = 1 和 x = –1 ):
In [30]: solve(x**20 - 1, x)
Out[30]:
[-1,
1,
-sqrt(-sqrt(5)/8 + 5/8) + I/4 + sqrt(5)*I/4,
-sqrt(-sqrt(5)/8 + 5/8) - sqrt(5)*I/4 - I/4,
sqrt(-sqrt(5)/8 + 5/8) + I/4 + sqrt(5)*I/4,
sqrt(-sqrt(5)/8 + 5/8) - sqrt(5)*I/4 - I/4,
-sqrt(sqrt(5)/8 + 5/8) - I/4 + sqrt(5)*I/4,
-sqrt(sqrt(5)/8 + 5/8) - sqrt(5)*I/4 + I/4,
sqrt(sqrt(5)/8 + 5/8) - I/4 + sqrt(5)*I/4,
sqrt(sqrt(5)/8 + 5/8) - sqrt(5)*I/4 + I/4]
In [31]: coeffs = np.zeros(21); \
....: coeffs[0] = 1; \
....: coeffs[20] = -1; \
....: p = np.poly1d(coeffs); \
....: p1prime = p.deriv(m=1); \
....: p2prime = p.deriv(m=2)
In [32]: newton(p, 0.5, fprime=p1prime)
RuntimeError: Failed to converge after 50 iterations, value is 2123.26621974
In [33]: newton(p, 0.5, fprime=p1prime, fprime2=p2prime)
RuntimeError: Failed to converge after 50 iterations, value is 2.65963902147
还有另一种技术可以通过定点迭代来迭代*似非线性标量方程的解。 例如,当我们的方程式可以写成 x = g(x)的形式时,这非常方便,因为方程式的解将是函数 g 的不动点。 。
通常,对于任何给定的方程 f(x)= 0 ,总有一种方便的方法将其重写为定点问题 x = g(x)。 当然,标准方法是写 g(x)= x + f(x),但这不一定提供最佳设置。 还有许多其他可能性。
为了计算到固定点的迭代,我们在模块scipy.optimize中具有例程fixed_point。 此实现基于 Steffensen 的算法,并使用 Aitken 的智能收敛加速:
In [34]: def g(t): return np.sin(t)/t + t
In [35]: from scipy.optimize import fixed_point
In [36]: fixed_point(g, 1)
Out[36]: 3.1415926535897913
布伦特法
由 Brent,Dekker 和 van Wijngaarten 开发的将割线和二等分方法与逆插值相结合的算法更加先进(且速度更快)。 在模块scipy.optimize中,我们有此算法的两个变体:brentq(使用逆二次插值)和brenth(使用反双曲插值)。 它们都以包围方法开始,并且需要一个包含函数 f(x)的根的间隔作为输入。
让我们用公式 si n(x)/ x = 0 比较 Brent 方法与包围方法的这两种变化:
In [37]: soln, info = bisect(f, 1, 5, full_output=True); \
....: list1 = ['bisect', info.root, info.iterations,
....: info.function_calls]
In [38]: soln, info = ridder(f, 1, 5, full_output=True); \
....: list2 = ['ridder', info.root, info.iterations,
....: info.function_calls]
In [39]: from scipy.optimize import brentq, brenth
In [40]: soln, info = brentq(f, 1, 5, full_output=True); \
....: list3 = ['brentq', info.root, info.iterations,
....: info.function_calls]
In [41]: soln, info = brenth(f, 1, 5, full_output=True); \
....: list4 = ['brenth', info.root, info.iterations,
....: info.function_calls]
In [42]: for item in [list1, list2, list3, list4]:
....: print "{0}: x={1}. Iterations: {2}. Calls: {3}".format(*item)
....:
bisect: x=3.14159265359\. Iterations: 42\. Calls: 44
ridder: x=3.14159265359\. Iterations: 5\. Calls: 12
brentq: x=3.14159265359\. Iterations: 10\. Calls: 11
brenth: x=3.14159265359\. Iterations: 10\. Calls: 11
非线性方程组
在本部分中,我们旨在找到标量或多元函数系统的解, F(X)= 0 ,其中 F 表示有限个数的 N 函数,每个函数都接受一个尺寸为 N 的向量 X 作为变量。
在代数或超越方程组的情况下,符号操作是可能的。 当尺寸太大时,这仍然是不切实际的。 一些例子可以说明这一点。
让我们从一个非常容易解决的情况开始,这种情况很容易消除:圆的交点( x 2 + y 2 = 16 ),并带有抛物线( x 2 – 2y = 8 ):
In [1]: import numpy as np; \
...: from sympy import symbols, solve
In [2]: x,y = symbols('x y', real=True)
In [3]: solutions = solve([x**2 + y**2 - 16, x**2 - 2*y -8])
In [4]: for item in solutions:
...: print '({0}, {1})'.format(item[x], item[y])
...:
(0, -4)
(0, -4)
(-2*sqrt(3), 2)
(2*sqrt(3), 2)
现在,让我们给出一个更困难的例子。 其中一个方程是分数式,另一个是多项式: 1 / x 4 + 6 / y 4 = 6,2y 4 + 12x 4 = 12x 4 和 4 :
In [5]: solve([1/x**4 + 6/y**4 - 6, 2*y**4 + 12*x**4 - 12*x**4*y**4])
Out[5]: []
没有解决办法吗? ( 1,(6 / 5)) 1/4 怎么样?
In [5]: x0, y0 = 1., (6/5.)**(1/4.)
In [6]: np.isclose(1/x0**4 + 6/y0**4, 6)
Out[6]: True
In [7]: np.isclose(2*y0**4 + 12*x0**4, 12*x0**4*y0**4)
Out[7]: True
只有迭代方法才能保证准确而快速的解决方案,而不会耗尽我们的计算资源。 让我们探索这个方向上的一些技术。
提示
从一个变量到多个变量带来了许多计算挑战。 在这种情况下出现的一些技术是对上一节中针对标量函数解释的方法的概括,但是还有许多其他策略可以利用大尺寸空间的更丰富结构。 与采用迭代方法求解线性方程组的情况一样,所有这些技术的命令都涉及学习非常高级的主题,例如功能分析中的算子,谱理论,Krylov 子空间等。 这远远超出了我们本书的范围。
有关所有方法的完整描述和分析,初始猜测的最佳选择或成功的预处理器的构造(使用时),请参考 Ortega 等人撰写的多个变量中非线性方程的迭代解。 莱茵堡。 它于 1970 年由 Academic Press 发行为《计算科学和应用数学专着》,目前仍是该主题的最佳可用资源之一。
为了分析非线性方程组,我们将在一个特别具有挑战性的示例上运行所有不同的方法,以尝试确定 x = [x [0],...,x [8]] 的值 ],求解以下三对角方程组:
(3-2*x[0])*x[0] -2*x[1] = -1
-x(k-1) + (3-2*x[k])*x[k] -2*x[k+1] = -1, k=1,...,7
-x[7] + (3-2*x[8])*x[8] = -1
我们可以将这样的系统定义为纯 NumPy 函数或 SymPy 矩阵函数(这将有助于我们将来计算其 Jacobian):
In [8]: def f(x):
...: output = [(3-2*x[0])*x[0] - 2*x[1] + 1]
...: for k in range(1,8):
...: output += [-x[k-1] + (3-2*x[k])*x[k] - 2*x[k+1] + 1]
...: output += [-x[7] + (3-2*x[8])*x[8] + 1]
...: return output
...:
In [9]: from sympy import Matrix, var
In [10]: var('x:9'); \
....: X = [x0, x1, x2, x3, x4, x5, x6, x7, x8]
In [11]: F = Matrix(f(X)); \
....: F
Out[11]:
Matrix([
[ x0*(-2*x0 + 3) - 2*x1 + 1],
[-x0 + x1*(-2*x1 + 3) - 2*x2 + 1],
[-x1 + x2*(-2*x2 + 3) - 2*x3 + 1],
[-x2 + x3*(-2*x3 + 3) - 2*x4 + 1],
[-x3 + x4*(-2*x4 + 3) - 2*x5 + 1],
[-x4 + x5*(-2*x5 + 3) - 2*x6 + 1],
[-x5 + x6*(-2*x6 + 3) - 2*x7 + 1],
[-x6 + x7*(-2*x7 + 3) - 2*x8 + 1],
[ -x7 + x8*(-2*x8 + 3) + 1]])
可以使用模块scipy.optimize中的通用例程root调用所有可用的迭代求解器。 该例程需要将系统 F(x)= 0 的左侧表达式和初始猜测作为强制性参数。 要访问不同的方法,我们包括参数method,可以将其设置为以下任何选项:
linearmixing:对于线性混合,这是一种非常简单的迭代非精确牛顿法,该方法使用标量*似于雅可比行列式。diagbroyden:对于对角布罗伊登方法,另一种简单的迭代不精确牛顿方法是使用对角线布罗伊登*似于雅可比矩阵。excitingmixing:为进行激动人心的混合,可以使用另一种简单的不精确牛顿法,该方法使用对角线*似于雅可比方程。broyden1:好的 Broyden 方法是使用 Broyden 的第一个 Jacobian *似的强大不精确牛顿方法。hybr:鲍威尔(Powell)的混合方法,是 SciPy 堆栈中最通用,最可靠的求解器,尽管对于大尺寸系统而言效率不高。broyden2:不好的 Broyden 方法类似于很好的 Broyden 方法,是另一种不精确的牛顿方法,它使用了 Broyden 的第二个 Jacobian *似。 它更适合于大型系统。krylov:牛顿-克雷洛夫方法是另一种不精确的牛顿法,其基于雅可比逆的克雷洛夫*似。 它是大尺寸系统的首选。anderson:这是安德森混合方法的扩展版本。 与 Newton-Krylov 和错误的 Broyden 方法一起,这是处理大规模非线性方程组的另一种选择武器。
实现非常聪明。 除 Powell 混合方法的情况外,其余代码对f(x)和Jacf(x)的(*似)雅可比行列使用相同的代码,但表达式不同。 为此,在模块scipy.optimize.nonlin中存储了一个带有以下类属性的 Python 类Jacobian:
.solve(v):对于合适的左侧向量v,它返回表达式 Jacf(x)^(-1) v*.update(x, F):这会将对象更新为x,并带有残余F(x),以确保在每个步骤的正确位置评估雅可比矩阵.matvec(v):对于合适的向量v,它返回乘积Jacf(x)*v.rmatvec(v):对于合适的向量v,它返回乘积Jacf(x).H*v.rsolve(v):对于合适的向量v,它返回乘积(Jacf(x).H)^(-1)*v.matmat(M):对于具有适当尺寸的密集矩阵M,这将返回矩阵乘积Jacf(x).H*M.todense():如果需要,可以形成密集的雅可比矩阵
我们很少需要担心在此类中创建对象。 例程root接受任何ndarray,稀疏矩阵,LinearOperator或什至其输出是先前任何输出的可调用对象作为 Jacobian。 它使用asjacobian方法在内部将其转换为Jacobian类。 此方法也托管在子模块scipy.optimize.nonlin中。
简单的迭代求解器
在 SciPy 堆栈中,我们有三个非常简单的不精确的 Newton 求解器,就像标量方程的割线方法一样,用合适的*似值替代了多元函数的 Jacobian 函数。 这些是线性和激励混合的方法,以及对角 Broyden 方法。 它们速度很快,但并不总是可靠-使用它们需要您自担风险!
为了深入分析这些求解器的速度和行为,我们将使用回调函数来存储收敛步骤。 一,线性混合的方法:
In [12]: from scipy.optimize import root
In [13]: root(f, np.zeros(9), method='linearmixing')
Out[13]:
status: 2
success: False
fun: array([ 9.73976997e+00, -1.66208587e+02, 7.98809260e+00,
-1.66555288e+01, 6.09078392e+01, -5.57689008e+03,
5.72527250e+06, -2.61478262e+13, 3.15410157e+06])
x: array([ 2.85055795e+00, -8.21972867e+00, 2.28548187e+00,
-1.17938653e+00, 4.52499108e+00, -4.30522840e+01,
8.68604963e+02, -3.61578590e+06, 4.81211473e+02])
message: 'The maximum number of iterations allowed has been reached.'
nit: 1000
这不太有希望! 如果我们愿意,我们可以以不同的公差或允许的最大迭代次数进行测试。 我们可以为该算法更改的另一种选择是在雅可比*似中搜索最佳线的方法。 这有助于确定在由所述*似给出的方向上的步长。 此时,我们只有三个选择:armijo(阿米乔-戈尔德斯坦条件,默认设置),wolfe(使用菲利普·沃尔夫不等式)或None。
传递给任何方法的所有选项都必须通过 Python 字典,通过参数options完成:
In [14]: lm_options = {}; \
....: lm_options['line_search'] = 'wolfe'; \
....: lm_options['xtol'] = 1e-5; \
....: lm_options['maxiter'] = 2000
In [15]: root(f, np.zeros(9), method='linearmixing',
....: options=lm_options)
OverflowError: (34, 'Result too large')
现在,让我们尝试在相同的初始条件下激发混合的方法:
In [16]: root(f, np.zeros(9), method='excitingmixing')
Out[16]:
status: 2
success: False
fun: array([ 1.01316841e+03, -8.25613756e+05, 4.23367202e+02,
-7.04594503e+02, 5.53687311e+03, -2.85535494e+07,
6.34642518e+06, -3.11754414e+13, 2.87053285e+06])
x: array([ 1.24211360e+01, -6.41737121e+02, 1.20299207e+01,
-1.69891515e+01, 3.26672949e+01, -3.77759320e+03,
8.82115576e+02, -3.94812801e+06, 7.34779049e+02])
message: 'The maximum number of iterations allowed has been reached.'
nit: 1000
类似的(缺乏)成功! 微调此方法的相关选项是line_search,浮点值alpha(将-1/alpha用作对 Jacobian 的初始*似值)和浮点值alphamax(因此,条目 对角雅可比行列式的范围保持在[alpha,alphamax]范围内。
让我们尝试使用初始条件相同的对角线 Broyden 方法:
In [17]: root(f, np.zeros(9), method='diagbroyden')
Out[17]:
status: 2
success: False
fun: array([-4.42907377, -0.87124314, -2.61646043, 0.59009568, -1.34073061,
-2.06266247, -0.32076522, 0.25120731, 0.0731001 ])
x: array([ 2.09429178, 1.46991649, -0.06730407, 0.96778603, 0.75367344,
1.2489588 , 1.46803463, 0.08282948, -0.24223748])
message: 'The maximum number of iterations allowed has been reached.'
nit: 1000
这种方法的性能也很差! 如果需要,我们可以尝试使用选项line_search和alpha来尝试改善收敛性。
布罗伊登法
好的 Broyden 方法是另一种不精确的 Newton 方法,该方法在第一次迭代中使用实际的 Jacobian 函数,但对于后续的迭代,则使用连续的秩一更新。 让我们看看我们的运行示例是否还有更多运气:
In [18]: root(f, np.zeros(9), method='broyden1')
Out[18]:
status: 2
success: False
fun: array([-111.83572901, -938.30236242, -197.71489446, -626.93927637,
-737.43130888, -19.87676004, -107.31583876, -92.32200167,
-252.26714229])
x: array([ 6.65222472, 22.1441079 , 9.17971608, 17.78778014,
19.65632798, 3.43502682, -6.03665297, 6.94424738, 11.87312669])
message: 'The maximum number of iterations allowed has been reached.'
nit: 1000
为了微调此算法,除了line_search和alpha之外,我们还可以控制在连续迭代中强制执行秩约束的方法。 我们可以使用可选的整数max_rank明确地限制等级不高于给定的阈值。 但是更好的是,我们可以采用一种依赖于其他因素的简化方法。 为此,我们使用选项reduce_method。
这些是选项:
-
restart:此归约方法删除所有矩阵列。 -
simple:仅删除最旧的矩阵列。 -
svd:与可选整数to_retain一起,此归约方法仅保留最重要的 SVD 分量(最多保留整数to_retain)。 如果强加整数max_rank,则to_retain的较好选择通常是max_rank - 2:In [19]: b1_options = {}; \ ....: b1_options['max_rank'] = 4; \ ....: b1_options['reduce_method'] = 'svd'; \ ....: b1_options['to_retain'] = 2 In [20]: root(f, np.zeros(9), method='broyden1', options=b1_options) Out[20]: status: 2 success: False fun: array([ -1.22226719e+00, -6.72508500e-02, -6.31642766e-03, -2.24588204e-04, -1.70786962e-05, -4.55208297e-05, -4.81332054e-06, 1.42432661e-05, -1.64421441e-05]) x: array([ 0.87691697, 1.65752568, -0.16593591, -0.60204322, -0.68244063, -0.688356 , -0.66512492, -0.59589812, -0.41638642]) message: 'The maximum number of iterations allowed has been reached.' nit: 1000
鲍威尔的混合求解器
在最成功的非线性系统求解器中,我们拥有 Powell 的混合算法,为此,在库MINPACK中有几个名为HYBRID**的 Fortran 例程。 这些例程实现了原始算法的多个修改版本。
当scipy例程根与method='hybr'一起调用时,它既充当HYBRID和HYBRIDJ的包装。 如果通过可选参数jac提供了 Jacobian 表达式,则 root 调用HYBRIDJ,否则调用HYBRID。 HYBRID代替了实际的雅可比行列式,而是使用在起点由正向差异构造的该算符的*似值。
提示
有关作者的鲍威尔混合算法的完整描述和分析,请参阅文章非线性方程式的混合方法,该文章于 1970 年在《非线性代数方程的数值方法》杂志上发表。
有关 Fortran 例程HYBRID和HYBRIDJ的实现细节,请参阅 MINPACK 用户指南的第 4 章,位于 http://www.mcs.anl.gov/~more/ANL8074b.pdf 。
让我们用难以捉摸的例子再试一次:
In [21]: solution = root(f, np.zeros(9), method='hybr')
In [22]: print solution.message
The solution converged.
In [23]: print "The root is approximately x = {0}".format(solution.x)
The root is approximately x = [-0.57065451 -0.68162834 -0.70173245 -0.70421294 -0.70136905 -0.69186564
-0.66579201 -0.5960342 -0.41641206]
In [24]: print "At that point, it is f(x) = {0}".format(solution.fun)
At that point, it is f(x) = [ -5.10793630e-11 1.00466080e-10 -1.17738708e-10 1.36598954e-10
-1.25279342e-10 1.10176535e-10 -2.81137336e-11 -2.43449705e-11
3.32504024e-11]
至少获得解决方案令人耳目一新,但我们可以做得更好。 让我们观察一下提供 f(x)的精确雅可比矩阵时method='hybr'的行为。 在我们的例子中,该运算符可以很容易地通过符号和数值计算,如下所示:
In [25]: F.jacobian(X)

In [26]: def Jacf(x):
....: output = -2*np.eye(9, k=1) - np.eye(9, k=-1)
....: np.fill_diagonal(output, 3-4*x)
....: return output
....:
In [27]: root(f, np.zeros(9), jac=Jacf, method='hybr')
status: 1
success: True
qtf: array([ -1.77182781e-09, 2.37713260e-09, 2.68847440e-09,
-2.24539710e-09, 1.34460264e-09, 8.25783813e-10,
-3.43525370e-09, 2.36025536e-09, 1.16245070e-09])
nfev: 25
r: array([-5.19829211, 2.91792319, 0.84419323, -0.48483853, 0.53965529,
-0.10614628, 0.23741206, -0.03622988, 0.52590331, -4.93470836,
2.81299775, 0.2137127 , -0.96934776, 1.03732374, -0.71440129,
0.27461859, 0.5399114 , 5.38440026, -1.62750656, -0.6939511 ,
0.3319492 , -0.11487171, 1.11300907, -0.65871043, 5.3675704 ,
-2.2941419 , -0.85326984, 1.56089518, -0.01734885, 0.12503146,
5.42400229, -1.8356058, -0.64571006, 1.61337203, -0.18691851,
5.25497284, -2.34515389, 0.34665604, 0.47453522, 4.57813558,
-2.82915356, 0.98463742, 4.64513056, -1.59583822, -3.76195794])
fun: array([ -5.10791409e-11, 1.00465636e-10, -1.17738708e-10,
1.36598732e-10, -1.25278898e-10, 1.10176535e-10,
-2.81135115e-11, -2.43454146e-11, 3.32505135e-11])
x: array([-0.57065451, -0.68162834, -0.70173245, -0.70421294, -0.70136905,
-0.69186564, -0.66579201, -0.5960342 , -0.41641206])
message: 'The solution converged.'
fjac: array([[-0.96956077, 0.19053436, 0.06633131, -0.12548354, 0.00592579,
0.0356269 , 0.00473293, -0.0435999 , 0.01657895],
[-0.16124306, -0.95068272, 0.1340795 , -0.05374361, -0.08570706,
0.18508814, -0.04624209, -0.05739585, 0.04797319],
[ 0.08519719, 0.11476118, 0.97782789, -0.0281114 , -0.08494929,
-0.05753056, -0.02702655, 0.09769926, -0.04280136],
[-0.13529817, -0.0388138 , 0.03067186, 0.97292228, -0.12168962,
-0.10168782, 0.0762693 , 0.0095415 , 0.04015656],
[-0.03172212, -0.09996098, 0.07982495, 0.10429531, 0.96154001,
-0.12901939, -0.13390792, 0.10972049, 0.02401791],
[ 0.05544828, 0.17833604, 0.03912402, 0.1374237 , 0.09225721,
0.93276861, -0.23865212, -0.00446867, 0.09571999],
[ 0.03507942, 0.00518419, 0.07516435, -0.0317367 , 0.17368453,
0.20035625, 0.9245396 , -0.20296261, 0.16065313],
[-0.05145929, -0.0488773 , -0.08274238, -0.02933344, -0.06240777,
0.09193555, 0.21912852, 0.96156966, -0.04770545],
[-0.02071235, -0.03178967, -0.01166247, 0.04865223, 0.05884561,
0.12459889, 0.11668282, -0.08544005, -0.97783168]])
njev: 2
观察到明显的改进-我们到达了相同的根,但仅进行了 25 次迭代。 只需要对雅可比矩阵进行两次评估。
大型求解器
对于大型系统,混合方法效率很低,因为该方法的强度取决于密集雅可比矩阵逆矩阵的内部计算。 在这种情况下,我们倾向于使用更健壮的不精确牛顿法。
其中一种是不良的 Broyden 方法(broyden2)。 安德森混合(anderson)也是一种可靠的方法。 但是,毫无疑问,最成功的方法是 Newton-Krylov 方法(krylov):
In [28]: root(f, np.zeros(9), method='krylov')
Out[28]:
status: 1
success: True
fun: array([ -8.48621595e-09, -1.28607913e-08, -9.39627043e-10,
-6.71023681e-10, -1.12563803e-09, -3.46839557e-09,
-7.64257968e-09, -1.29112268e-08, -1.26301001e-08])
x: array([-0.57065452, -0.68162834, -0.70173245, -0.70421294, -0.70136905,
-0.69186565, -0.66579202, -0.5960342 , -0.41641207])
message: 'A solution was found at the specified tolerance.'
nit: 29
我们仅在29迭代中就获得了很好的*似值。 通过一系列可选参数可以进行改进。 两个关键选项是:
-
来自模块
scipy.sparse.linalg的线性方程的迭代求解器,用于计算雅可比方程的 Krylov *似 -
A preconditioner for the inner Krylov iteration: a functional expression that approximates the inverse of the Jacobian
提示
为了说明预处理器的使用,SciPy 的官方文档中有一个很好的例子,网址为 http://docs.scipy.org/doc/scipy/reference/tutorial/optimize.html 。
进一步解释这两个选项的用法将需要一本教科书! 有关此技术背后的理论,请参阅 D.A.发表的无 Jacobian 牛顿-克里洛夫方法文章。 诺尔和 D.E. 《计算物理学杂志》上的凯斯。 193,357(2003)。
优化
最好将优化问题描述为搜索标量值函数 f(x)的局部最大值或最小值。 可以针对 f 域中的所有可能的输入值(在这种情况下,我们将此问题称为无约束优化)或可以由 a 表示的特定子集执行此搜索。 有限的恒等式和不等式(并且我们将此另一个问题称为约束优化)。 在本节中,我们将在几种设置中探索两种模式。
单变量函数的无约束优化
我们专注于在区间 [a,b] 中搜索函数 f(x)的局部最小值(对局部最大值的搜索可以视为对 函数 –f(x)在相同间隔中的局部最小值)。 对于此任务,我们在模块scipy.optimize中具有例程minimize_scalar。 它接受单变量函数 f(x)以及搜索方法作为强制输入。
尽管在此设置中括号的概念有些不同,但大多数搜索方法都基于我们用于查找根的括号的概念。 在这种情况下,好的括号是 x < y < z 的三元组,其中 f(y)小于两个 f(x) 和 f(z)。 如果函数是连续的,则其图形在括号中呈现 U 形。 这保证了子间隔 [x,z] 内的最小值。 一个成功的包围方法将在每个后续步骤中寻找 [x,y] 或 [y,z] 中的目标极值。
让我们构造一个简单的包围式测试方法。 假设我们有一个初始括号 a < c < b 。 通过二次插值,我们通过点(a,f(a)),(c,f(c))和(b,f( b))。 由于 U 形条件,插值抛物线必须有一个最小值(易于计算),例如(d,f(d))。 不难证明值d位于子间隔 [a,c] 和 [c,b] 的中点之间。 我们将在下一个包围步骤中使用这一点 d 。 例如,如果碰巧 c < d ,则下一个括号将是 c < d < b 或 a < c < d 。 很简单! 让我们实现这个方法:
In [1]: import numpy as np; \
...: from scipy.interpolate import lagrange; \
...: from scipy.optimize import OptimizeResult, minimize_scalar
In [2]: def good_bracket(func, bracket):
...: a, c, b = bracket
...: return (func(a) > func(c)) and (func(b) > func(c))
...:
In [3]: def parabolic_step(f, args, bracket, **options):
...: stop = False
...: funcalls = 0
...: niter = 0
...: while not stop:
...: niter += 1
...: interpolator = lagrange(np.array(bracket),
...: f(np.array(bracket)))
...: funcalls += 3
...: a, b, c = interpolator.coeffs
...: d = -0.5*b/a
...: if np.allclose(bracket[1], d):
...: minima = d
...: stop = True
...: elif bracket[1] < d:
...: newbracket = [bracket[1], d, bracket[2]]
...: if good_bracket(f, newbracket):
...: bracket = newbracket
...: else:
...: bracket = [bracket[0], bracket[1], d]
...: else:
...: newbracket = [d, bracket[1], bracket[2]]
...: if good_bracket(f, newbracket):
...: bracket = newbracket
...: else:
...: bracket = [bracket[0], d, bracket[1]]
...: return OptimizeResult(fun=f(minima), x=minima,
...: nit=niter, nfev=funcalls)
提示
任何最小化方法的输出都必须是OptimizeResult对象,至少具有属性x(优化问题的解决方案)。 在我们刚才运行的示例中,用此方法编码的属性是x,fun(在该解决方案中对 f 的求值),nit(迭代次数)和nfev(所需功能评估的次数)。
让我们在一些示例上运行此方法:
In [4]: f = np.vectorize(lambda x: max(1-x, 2+x))
In [5]: def g(x): return -np.exp(-x)*np.sin(x)
In [6]: good_bracket(f, [-1, -0.5, 1])
Out[6]: True
In [7]: minimize_scalar(f, bracket=[-1, -0.5, 1],
...: method=parabolic_step)
Out[7]:
fun: array(1.5000021457670878)
nfev: 33
nit: 11
x: -0.50000214576708779
In [8]: good_bracket(g, [0, 1.2, 1.5])
Out[8]: True
In [9]: minimize_scalar(g, bracket=[0,1.2,1.5],
...: method=parabolic_step)
Out[9]:
fun: -0.32239694192707441
nfev: 54
nit: 18
x: 0.78540558550495643
按照 Brent 和 Dekker 的算法,已经有两种编码用于单变量标量最小化的方法:golden(使用黄金分割搜索)和brent:
In [10]: minimize_scalar(f, method='brent', bracket=[-1, -0.5, 1])
Out[10]:
fun: array(1.5)
nfev: 22
nit: 21
x: -0.5
In [11]: minimize_scalar(f, method='golden', bracket=[-1, -0.5, 1])
Out[11]:
fun: array(1.5)
x: -0.5
nfev: 44
In [12]: minimize_scalar(g, method='brent', bracket=[0, 1.2, 1.5])
Out[12]:
fun: -0.32239694194483443
nfev: 11
nit: 10
x: 0.78539817180087257
In [13]: minimize_scalar(g, method='golden', bracket=[0, 1.2, 1.5])
Out[13]:
fun: -0.32239694194483448
x: 0.7853981573284226
nfev: 43
单变量函数的约束优化
尽管例程minimize_scalar中包含的括号已经对该函数施加了约束,但是可以在一个不容易找到括号的合适间隔内强制搜索一个真正的最小值:
In [14]: minimize_scalar(g, method='bounded', bounds=(0, 1.5))
Out[14]:
status: 0
nfev: 10
success: True
fun: -0.32239694194483415
x: 0.78539813414299553
message: 'Solution found.'
多元函数的无约束优化
除了通过蛮力或盆跳最小化的情况外,我们可以使用模块scipy.optimize中的通用例程minimize执行所有其他搜索。 参数方法与其单变量对应方法一样,需要选择用于实现极值的算法。 已经编码了几种众所周知的算法,但是我们也可以通过合适的定制方法来实现自己的想法。
在本节中,我们将重点介绍编码实现的描述和用法。 在我们为minimize_scalar的自定义方法的构造中采用的相同技术在这里是有效的,但额外尺寸带来了明显的挑战。
为了比较所有不同的方法,我们将它们与一个特别具有挑战性的函数进行比较:Rocksenbrock 的抛物线谷(也非正式地称为香蕉函数)。 模块scipy.optimize具有此功能的 NumPy 版本,以及其 Jacobian 和 Hessian:
In [15]: from scipy.optimize import rosen; \
....: from sympy import var, Matrix, solve, pprint
In [16]: var('x y')
Out[16]: (x, y)
In [17]: F = Matrix([rosen([x, y])]); \
....: pprint(F)
[(-x + 1)<sup>2</sup> + 100.0(-x<sup>2</sup> + y)<sup>2</sup>]
In [18]: X, Y = np.mgrid[-1.25:1.25:100j, -1.25:1.25:100j]
In [19]: def f(x,y): return rosen([x, y])
In [20]: import matplotlib.pyplot as plt, matplotlib.cm as cm; \
....: from mpl_toolkits.mplot3d.axes3d import Axes3D
In [21]: plt.figure(); \
....: plt.subplot(121, aspect='equal'); \
....: plt.contourf(X, Y, f(X,Y), levels=np.linspace(0,800,16),
....: cmap=cm.Greys)
....: plt.colorbar(orientation='horizontal')
....: plt.title('Contour plot')
....: ax = plt.subplot(122, projection='3d', aspect='equal')
....: ax.plot_surface(X, Y, f(X,Y), cmap=cm.Greys, alpha=0.75)
....: plt.colorbar(orientation='horizontal')
....: plt.title('Surface plot')
....: plt.show()

该图显示了可能包含局部最小值的大区域(香蕉形状)。 多元演算技术可以帮助我们精确地确定所有关键点,而不必依靠直觉。 我们首先需要计算函数的雅可比行列式和黑森州式:
In [22]: JacF = F.jacobian([x, y]); \
....: pprint(JacF)
[- 400.0⋅x⋅(- x<sup>2</sup> + y) + 2⋅x - 2 - 200.0⋅x<sup>2</sup> + 200.0⋅y]
In [23]: HesF = JacF.jacobian([x, y]); \
....: pprint(HesF)
[1200.0⋅x<sup>2</sup> - 400.0⋅y + 2 -400.0⋅x
-400.0⋅x 200.0 ]
In [24]: solve(JacF)
Out[24]: [{x: 1.00000000000000, y: 1.00000000000000}]
In [25]: HesF.subs({x: 1.0, y: 1.0})
Out[25]:
Matrix([
[ 802.0, -400.0],
[-400.0, 200.0]])
In [26]: _.det()
Out[26]: 400.000000000000
这些计算表明在(1, 1)仅存在一个临界点。 毫无疑问,这一点表示该位置的局部最小值。
尽管可行,但尝试使用此技术针对更高维度的 Rosenbrock 函数计算临界点的计算量很大。 例如,移至四个维度需要一台像样的计算机大约半分钟:
In [27]: var('x:4'); \
....: X = [x0, x1, x2, x3]; \
....: F = Matrix([rosen(X)])
In [28]: %time solve(F.jacobian(X))
CPU times: user 36.6 s, sys: 171 ms, total: 36.8 s
Wall time: 36.7 s
Out[28]:
[{x<sub>0</sub>:1.0,x<sub>1</sub>:1.0,x<sub>2</sub>:1.0,x<sub>3</sub>:1.0}]
对于大尺寸,可以通过蛮力来寻找全局最小值。 不是很优雅,但是可以完成工作。 蛮力算法能够跟踪全局最小值(或使其*似到令人满意的精度)。 我们可以使用模块scipy.optimize中的例程brute调用此方法。 强制性参数是要最小化的功能,以及我们将应用优化的域的描述。 最好将此域编码为切片的元组。 例如,要在四个变量中搜索 Rosenbrock 函数的全局最小值,其中每个变量的绝对值都以三个为界,我们可以发出以下命令:
In [29]: from scipy.optimize import brute
In [30]: interval = slice(-3, 3, 0.25); \
....: box = [interval] * 4
In [31]: %time brute(rosen, box)
CPU times: user 13.7 s, sys: 6 ms, total: 13.7 s
Wall time: 13.7 s
Out[31]: array([ 1., 1., 1., 1.])
还是一个很慢的过程! 为了达到速度,最好使用迭代方法。 根据几种模式(以及这些模式的组合)可以在此设置中搜索最小值:
- 随机方法:这些方法适合于搜索实际的全局最小值。 它们生成并使用随机变量。 在模块
scipy.optimize中,我们有两个此类的指数:- 一种是盆地跳跃算法,在模块
scipy.optimize中由例程basinhopping调用。 该实现具有由 Metropolis 标准蒙特卡洛模拟给出的验收测试。 - 另一个是不推荐使用的模拟退火方法,称为
method='Anneal'。 这是蒙特卡洛模拟的一种变体。 对于搜索空间离散且较大的优化问题很有用。
- 一种是盆地跳跃算法,在模块
- 确定性算法,仅使用函数评估:这些算法基本上是通过沿不同方向进行连续线性最小化来执行的。 在模块
scipy.optimize中,我们有两种符合此原则的方法:- 基于一维布伦特极小化的 Powell 方法。 我们称它为
method='Powell'。 - 下坡单纯形算法,也称为变形虫方法,由 Nelder 和 Mead 在 1965 年创建。我们将其称为
method='Nelder-Mead'。
- 基于一维布伦特极小化的 Powell 方法。 我们称它为
- 牛顿法:这些是关于微分函数的确定性算法,该算法模仿多元演算来搜索关键点。 简而言之,我们寻求至少一个临界点,其 Hessians 满足局部极小值的条件。 这些算法同时采用了 Jacobian 和 Hessian 评估。 通常,由于这些表达式的复杂性,通常改为对这两个运算符进行*似处理。 在这种情况下,我们将这些方法称为准牛顿法。 在模块
scipy.optimize中,我们有 Broyden,Fletcher,Goldfarb 和 Shanno 的准牛顿法( BFGS ),该方法仅使用一阶导数。 我们称它为method='BFGS'。 - 共轭梯度法:这里,我们有三种变体:
- Fetcher-Reeves 算法的一种变体是实现由 Polak 和 Ribiere 编写的纯共轭梯度。 它仅使用一阶导数,并与
method ='CG'一起调用。 - 共轭梯度与牛顿法(即截断的牛顿法)的组合,我们称之为
method='Newton-CG'。 - 牛顿共轭梯度信任区域算法的两种不同版本,它们使用信任区域的思想更有效地限制了可能极小值的位置。 我们称它们为
method='dogleg'和method='trust-ncg'。
- Fetcher-Reeves 算法的一种变体是实现由 Polak 和 Ribiere 编写的纯共轭梯度。 它仅使用一阶导数,并与
让我们浏览这些方法。
随机方法
让我们使用盆地跳跃技术找到九个变量的 Rosenbrock 函数的全局最小值:
In [32]: from scipy.optimize import minimize, basinhopping
In [33]: %time basinhopping(rosen, np.zeros(9))
CPU times: user 4.59 s, sys: 7.17 ms, total: 4.6 s
Wall time: 4.6 s
Out[33]:
nfev: 75633
minimization_failures: 52
fun: 2.5483642615054407e-11
x: array([ 0.99999992, 0.99999994, 0.99999992, 0.99999981, 0.99999962,
0.99999928, 0.99999865, 0.9999972 , 0.99999405])
message: ['requested number of basinhopping iterations completed successfully']
njev: 6820
nit: 100
让我们比较一下(不建议使用的)模拟退火的行为:
In [34]: minimize(rosen, np.zeros(9), method='Anneal')
Out[34]:
status: 5
success: False
accept: 19
nfev: 651
T: 1130372817.0369582
fun: 707171392.44894326
x: array([ 11.63666756, -24.41186725, 48.26727994, 3.97730959,
-31.52658563, 18.00560694, 1.22589971, 21.97577333, -43.9967434 ])
message: 'Final point not the minimum amongst encountered points'
nit: 12
专门采用功能评估的确定性算法
让我们将 Powell 方法的结果与下坡单纯形方法进行比较:
In [35]: minimize(rosen, np.zeros(9), method='Powell')
Out[35]:
status: 0
success: True
direc: array([[ -9.72738085e-06, 2.08442100e-05, 2.06470355e-05,
4.39487337e-05, 1.29109966e-04, 1.98333214e-04,
3.66992711e-04, 7.00645878e-04, 1.38618490e-03],
[ -6.95913466e-06, -7.25642357e-07, -2.39771165e-06,
4.10148947e-06, -6.17293950e-06, -6.53887928e-06,
-1.06472130e-05, -5.23030557e-06, -2.28609232e-06],
[ 0.00000000e+00, 0.00000000e+00, 1.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00],
[ 1.23259262e-06, 9.30817407e-07, 2.48075497e-07,
-7.07907998e-07, -2.01233105e-07, -1.10513430e-06,
-2.57164619e-06, -2.58316828e-06, -3.89962665e-06],
[ 6.07328675e-02, 8.51817777e-02, 1.30174960e-01,
1.71511253e-01, 9.72602622e-02, 1.47866889e-02,
1.12376083e-03, 5.35386263e-04, 2.04473740e-04],
[ 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 1.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00],
[ 3.88222708e-04, 8.26386166e-04, 5.56913200e-04,
3.08319925e-04, 4.45122275e-04, 2.66513914e-03,
6.31410713e-03, 1.24763367e-02, 2.45489699e-02],
[ 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
0.00000000e+00, 1.00000000e+00, 0.00000000e+00],
[ -2.82599868e-13, 2.33068676e-13, -4.23850631e-13,
-1.23391999e-12, -2.41224441e-12, -5.08909225e-12,
-9.92053051e-12, -2.07685498e-11, -4.10004188e-11]])
nfev: 6027
fun: 3.1358222279861171e-21
x: array([ 1., 1., 1., 1., 1., 1., 1., 1., 1.])
message: 'Optimization terminated successfully.'
nit: 56
In [36]: minimize(rosen, np.zeros(9), method='Nelder-Mead')
status: 1
nfev: 1800
success: False
fun: 4.9724099905503065
x: array([ 0.85460488, 0.70911132, 0.50139591, 0.24591886, 0.06234451,
-0.01112426, 0.02048509, 0.03266785, -0.01790827])
message: 'Maximum number of function evaluations has been exceeded.'
nit: 1287
Broyden-Fletcher-Goldfarb-Shanno 拟牛顿法
让我们在运行示例中观察该算法的行为:
In [37]: minimize(rosen, np.zeros(9), method='BFGS')
Out[37]:
status: 0
success: True
njev: 83
nfev: 913
hess_inv: array([[ 1.77874730e-03, 1.01281617e-03, 5.05884211e-04,
3.17367120e-04, 4.42590321e-04, 7.92168518e-04,
1.52710497e-03, 3.06357905e-03, 6.12991619e-03],
[ 1.01281617e-03, 1.91057841e-03, 1.01489866e-03,
7.31748268e-04, 8.86058826e-04, 1.44758106e-03,
2.76339393e-03, 5.60288875e-03, 1.12489189e-02],
[ 5.05884211e-04, 1.01489866e-03, 2.01221575e-03,
1.57845668e-03, 1.87831124e-03, 3.06835450e-03,
5.86489711e-03, 1.17764144e-02, 2.35964978e-02],
[ 3.17367120e-04, 7.31748268e-04, 1.57845668e-03,
3.13024681e-03, 3.69430791e-03, 6.16910056e-03,
1.17339522e-02, 2.33374859e-02, 4.66640347e-02],
[ 4.42590321e-04, 8.86058826e-04, 1.87831124e-03,
3.69430791e-03, 7.37204979e-03, 1.23036988e-02,
2.33709766e-02, 4.62512165e-02, 9.24474614e-02],
[ 7.92168518e-04, 1.44758106e-03, 3.06835450e-03,
6.16910056e-03, 1.23036988e-02, 2.48336778e-02,
4.71369608e-02, 9.29927375e-02, 1.85683729e-01],
[ 1.52710497e-03, 2.76339393e-03, 5.86489711e-03,
1.17339522e-02, 2.33709766e-02, 4.71369608e-02,
9.44348689e-02, 1.86490477e-01, 3.72360210e-01],
[ 3.06357905e-03, 5.60288875e-03, 1.17764144e-02,
2.33374859e-02, 4.62512165e-02, 9.29927375e-02,
1.86490477e-01, 3.73949424e-01, 7.46959044e-01],
[ 6.12991619e-03, 1.12489189e-02, 2.35964978e-02,
4.66640347e-02, 9.24474614e-02, 1.85683729e-01,
3.72360210e-01, 7.46959044e-01, 1.49726446e+00]])
fun: 6.00817150312557e-11
x: array([ 0.99999993, 0.99999986, 0.99999976, 0.99999955, 0.99999913,
0.99999832, 0.99999666, 0.99999334, 0.99998667])
message: 'Optimization terminated successfully.'
jac: array([ 5.23788826e-06, -5.45925187e-06, -1.35362172e-06,
8.75480656e-08, -9.45374358e-06, 7.31889131e-06,
3.34352248e-07, -7.24984749e-07, 2.02705630e-08])
注意
请注意,与 Powell 方法(包括 Jacobian 评估)相比,此方法采用了更多的迭代,但函数评估却少得多。 准确性是可比的,但是复杂性和速度方面的提升却非凡。
共轭梯度法
纯共轭梯度法最适合于具有清晰,唯一临界点且斜率范围不太大的函数。 多个固定点往往会使迭代混乱,并且太陡的斜率(大于 1000)会导致可怕的舍入误差。
在不提供雅可比行列式的表达式的情况下,该算法计算此算符的体面*似值以计算一阶导数:
In [38]: minimize(rosen, np.zeros(9), method='CG')
Out[38]:
status: 0
success: True
njev: 326
nfev: 3586
fun: 1.5035665428352255e-10
x: array([ 0.9999999 , 0.99999981, 0.99999964, 0.99999931, 0.99999865,
0.99999733, 0.9999947 , 0.99998941, 0.99997879])
message: 'Optimization terminated successfully.'
jac: array([ -1.48359492e-06, 2.95867756e-06, 1.71067556e-06,
-1.83617409e-07, -2.47616618e-06, -5.34951641e-06,
2.50389338e-06, -2.37918319e-06, -3.86667920e-06])
包括一个实际的 Jacobian 可以大大改善问题。 请注意,求出的最小值(fun)的评估有所改进:
In [39]: from scipy.optimize import rosen_der
In [40]: minimize(rosen, np.zeros(9), method='CG', jac=rosen_der)
Out[40]:
status: 0
success: True
njev: 406
nfev: 406
fun: 8.486856765134401e-12
x: array([ 0.99999998, 0.99999996, 0.99999994, 0.99999986, 0.99999969,
0.99999938, 0.99999875, 0.9999975 , 0.999995 ])
message: 'Optimization terminated successfully.'
jac: array([ 1.37934336e-06, -9.03688875e-06, 8.53289049e-06,
9.77779178e-06, -2.63022111e-06, -1.02087919e-06,
-6.55712127e-06, -1.71887373e-06, -9.12268328e-07])
截断的牛顿方法需要精确的 Jacobian 函数才能工作:
In [41]: minimize(rosen, np.zeros(9), method='Newton-CG')
ValueError: Jacobian is required for Newton-CG method
In [38]: minimize(rosen, np.zeros(9), method='Newton-CG', jac=rosen_der)
Out[41]:
status: 0
success: True
njev: 503
nfev: 53
fun: 5.231613200425767e-08
x: array([ 0.99999873, 0.99999683, 0.99999378, 0.99998772, 0.99997551,
0.99995067, 0.99990115, 0.99980214, 0.99960333])
message: 'Optimization terminated successfully.'
nhev: 0
jac: array([ 6.67155399e-06, 2.50927306e-05, 1.03398234e-04,
4.09953321e-04, 1.63524314e-03, 6.48667316e-03,
-1.91779902e-03, -2.81972861e-04, -5.67500380e-04])
使用信任区域的方法需要对 Hessian 进行精确表达:
In [42]: from scipy.optimize import rosen_hess
In [43]: minimize(rosen, np.zeros(9), method='dogleg',
....: jac=rosen_der, hess=rosen_hess)
Out[43]:
status: 0
success: True
njev: 25
nfev: 29
fun: 9.559277795967234e-19
x: array([ 1., 1., 1., 1., 1., 1., 1., 1., 1.])
message: 'Optimization terminated successfully.'
nhev: 24
jac: array([ 3.84137166e-14, 3.00870439e-13, 1.10489395e-12,
4.32831548e-12, 1.72455383e-11, 6.77315980e-11,
2.48459919e-10, 6.62723207e-10, -1.52775570e-09])
nit: 28
In [44]: minimize(rosen, np.zeros(9), method='trust-ncg',
....: jac=rosen_der, hess=rosen_hess)
Out[44]:
status: 0
success: True
njev: 56
nfev: 67
fun: 3.8939669818289621e-18
x: array([ 1., 1., 1., 1., 1., 1., 1., 1., 1.])
message: 'Optimization terminated successfully.'
nhev: 55
jac: array([ 2.20490293e-13, 5.57109914e-13, 1.77013959e-12,
-9.03965791e-12, -3.05174774e-10, 3.03425818e-09,
1.49134067e-08, 6.32240935e-08, -3.64210218e-08])
nit: 66
请注意,与以前的方法相比,在准确性,迭代和功能评估方面都取得了巨大的进步! 明显的缺点是,要获得 Jacobian 或 Hessian 算子的良好表示,通常是非常困难的。
多元函数的约束优化
以*面函数 f(x,y)= 5x – 2y + 4 在圆 x 2 + y 2 = 4 。 使用 SymPy,我们可以实现 Lagrange 乘法器的技术:
In [45]: F = Matrix([5*x - 2*y + 4]); \
....: G = Matrix([x**2 + y**2 - 4]) # constraint
In [46]: var('z'); \
....: solve(F.jacobian([x, y]) - z * G.jacobian([x, y]))
Out[46]: [{x: 5/(2*z), y: -1/z}]
In [47]: soln = _[0]; \
....: solve(G.subs(soln))
Out[47]: [{z: -sqrt(29)/4}, {z: sqrt(29)/4}]
In [48]: zees = _; \
....: [(soln[x].subs(item), soln[y].subs(item)) for item in zees]
Out[48]:
[(-10*sqrt(29)/29, 4*sqrt(29)/29), (10*sqrt(29)/29, -4*sqrt(29)/29)]
还不错! 在此约束之上,我们可以进一步以不等式的形式强加另一个条件。 考虑与之前相同的问题,但改为限制为半圆: y > 0 。 在这种情况下,新结果将仅是坐标为 x = –10√(29)/ 29 = –1.8569533817705186 和 y =4√(29)/ 29 = 0.74278135270820744 [ 。
当然,可以用数字方式解决这个问题。 在模块scipy.optimize中,我们基本上有三种方法,所有这些方法都可以从通用例程minimize中调用:
- 基于 BFGS 算法(我们称之为
method='L-BFGS-B')的大规模约束约束优化。 该实现实际上是由[Ciyou Zhu],[Richard Byrd]和[Jorge Nocedal]编写的同名FORTRAN例程的包装(有关详细信息,请参见 RH Byrd,P.Lu 和 J.Nocedal。[HTG2 用于有限约束优化的有限存储器算法,(1995), SIAM 科学与统计计算杂志,16,5,第 1190-1208 页)。 - 一种基于牛顿截断法的约束算法(我们称其为
method='TNC')。 此实现与我们用method='Newton-CG'调用的实现类似,除了此版本是C例程的包装。 - 通过线性*似进行约束优化(用
method='COBYLA'调用)。 此实现使用相同的名称包装FORTRAN例程。 - 一种基于顺序最小二乘编程(
method='SLSQP')的最小化方法。 该实现是 Dieter Kraft 编写的同名FORTRAN例程的包装。
让我们使用我们的运行示例来说明如何输入不同的约束。 我们将它们实现为字典或字典的元组—元组中的每个条目都表示同一性('eq')或不等式('ineq'),以及函数表达式(以ndarray的形式) 必要时)及其对应的派生词:
In [49]: def f(x): return 5*x[0] - 2*x[1] + 4
In [50]: def jacf(x): return np.array([5.0, -2.0])
In [51]: circle = {'type': 'eq',
....: 'fun': lambda x: x[0]**2 + x[1]**2 - 4.0,
....: 'jac': lambda x: np.array([2.0 * x[0],
2.0 * x[1]])}
In [52]: semicircle = ({'type': 'eq',
....: 'fun': lambda x: x[0]**2 + x[1]**2 - 4.0,
....: 'jac': lambda x: np.array([2.0 * x[0],
....: 2.0 * x[1]])},
....: {'type': 'ineq',
....: 'fun': lambda x: x[1],
....: 'jac': lambda x: np.array([0.0, 1.0])})
约束通过参数constraints馈送到例程minimize。 最初的猜测也必须满足约束条件,否则,算法将无法收敛到任何有意义的东西:
In [53]: minimize(f, [2,2], jac=jacf, method='SLSQP', constraints=circle)
Out[53]:
status: 0
success: True
njev: 11
nfev: 13
fun: -6.7703296142789693
x: array([-1.85695338, 0.74278135])
message: 'Optimization terminated successfully.'
jac: array([ 5., -2., 0.])
nit: 11
In [54]: minimize(f, [2,2], jac=jacf, method='SLSQP', constraints=semicircle)
Out[54]:
status: 0
success: True
njev: 11
nfev: 13
fun: -6.7703296142789693
x: array([-1.85695338, 0.74278135])
message: 'Optimization terminated successfully.'
jac: array([ 5., -2., 0.])
nit: 11
摘要
在本章中,我们已经掌握了计算数学中两个最具挑战性的过程-搜索函数的根和极值。 您了解了在几种设置中解决这些问题的符号方法和数字方法,以及如何通过收集有关函数的足够信息来避免常见的陷阱。
在下一章中,我们将探讨一些求解微分方程的技术。
五、常微分方程的初值问题
常微分方程(或系统)的初值问题几乎不需要任何动力。 它们几乎在所有科学中自然产生。 在本章中,我们将专注于掌握数值方法来求解这些方程式。
在本章中,我们将通过三个常见示例探索 SciPy 堆栈中实现的所有技术:
-
一阶微分方程 y'(t)= y(t),初始条件为 y(0)= 1 。 实际解为 y(t)= e t 。
-
一阶更复杂的微分方程:伯努利方程 ty'(t)+ 6y(t)= 3ty(t) 3/4 , 初始条件 y(1)= 1 。 实际解是 y(t)=(3t 5/2 + 7) 4 /(10000t 6 )。
-
To illustrate the behavior with autonomous systems (where the derivatives do not depend on the time variable), we use a Lotka-Volterra model for one predator and one prey, y0'(t) = y0(t) – 0.1 y0(t) y1(t) and y1'(t) = –1.5 y1(t) + 0.075 y0(t) y1(t) with the initial condition y0(0) = 10 and y1(0) = 5 (representing 10 prey and 5 predators at the initial time).
注意
高阶微分方程始终可以转换为(非必要自治的)微分方程系统。 反过来,微分方程的非自治系统总是可以通过以智能方式包含新变量而变成自治的。 完成这些转换的技术非常简单,并且在任何有关微分方程的教科书中都有介绍。
我们有可能通过 SymPy 解析地求解一些微分方程。 尽管这并不是解决计算初始值问题的最佳方法(即使有解析解可用),但我们将举例说明一些完成的示例。 可靠的求解器本质上是数值求解器,在这种情况下,主要有两种方法可以解决此问题-通过解析*似方法或离散变量方法。
微分方程的符号解
通过模块sympy.solvers.ode在 SciPy 堆栈中编码几种类型的微分方程的符号处理。 此时,使用此方法只能访问以下方程式:
- 一阶可分离
- 一阶齐次
- 一阶精确
- 一阶线性
- 一阶伯努利
- 二阶 Liouville
- 具有常数系数的任何阶数线性方程
除这些以外,其他方程式可以通过以下技术解决:
- 一阶或二阶方程的幂级数解(后者仅在普通和规则奇异点处)
- 一阶方程的李群方法
让我们结合我们的一维示例 y'= y 和伯努利方程,来实践这些技术。 注意输入微分方程的方法。 我们将其以 F(t,y,y')= 0 的形式编写,并将表达式 F(t,y,y')馈送到求解器(请参见第 4 行)。 3)。 另外,请注意我们如何使用 SymPy 编写函数的派生代码。 表达式f(t).diff(t)表示 f(t)的一阶导数,例如:
In [1]: from sympy.solvers import ode
In [2]: t = symbols('t'); \
...: f = Function('f')
In [3]: equation1 = f(t).diff(t) - f(t)
In [4]: ode.classify_ode(equation1)
Out[4]:
('separable',
'1st_exact',
'1st_linear',
'almost_linear',
'1st_power_series',
'lie_group',
'nth_linear_constant_coeff_homogeneous',
'separable_Integral',
'1st_exact_Integral',
'1st_linear_Integral',
'almost_linear_Integral')
注意
请注意,某些方法有一个带有后缀_Integral的变体。 这是一个聪明的机制,可让我们无需实际计算所需的积分即可完成解决方案。 当面对昂贵或不可能的积分时,这很有用。
该方程已被分类为几种类型的成员。 我们现在可以根据相应类型的适当技术来解决它。 例如,我们选择首先假设方程是可分离的,然后通过计算幂级数解的四阶*似( n=4 )来求解该方程。 x0=0周围的表示形式( hint='1st_power_series' ):
In [5]: ode.dsolve(equation1, hint='separable')
Out[5]: f(t) == C1*exp(t)
In [6]: ode.dsolve(equation1, hint='1st_power_series', n=4, x0=0.0)
Out[6]: f(t) == C0 + C0*t + C0*t**2/2 + C0*t**3/6 + O(t**4)
解决初始值问题也是可能的,但仅适用于以一阶微分方程的幂级数计算的解:
In [7]: ode.dsolve(equation1, hint='1st_power_series', n=3, x0=0,
...: ics={f(0.0): 1.0})
Out[7]: f(t) == 1.0 + 1.0*t + 0.5*t**2 + O(t**3)
让我们用这些技术探索第二个例子:
In [8]: equation2 = t*f(t).diff(t) + 6*f(t) - 3*t*f(t)**(0.75)
In [9]: ode.classify_ode(equation2)
Out[9]: ('Bernoulli', 'lie_group', 'Bernoulli_Integral')
In [10]: dsolve(equation2, hint='Bernoulli')
Out[10]: f(t) == (t**(-1.5)*(C1 + 0.3*t**2.5))**4.0
In [11]: dsolve(equation2, hint=lie_group')
Out[11]: f(t) == -5/(3*t*(C1*t**5 - 1))
[f(t) == 6.25e-6*(t**6*(625.0*C1**4 + 5400.0*C1*t**5 + 1296.0*t**10)
- 120.0*sqrt(C1*t**17*(25.0*C1 + 36.0*t**5)**2))/t**12,
f(t) == 6.25e-6*(t**6*(625.0*C1**4 + 5400.0*C1*t**5 + 1296.0*t**10)
+ 120.0*sqrt(C1*t**17*(25.0*C1 + 36.0*t**5)**2))/t**12]
当然,尽管第 10 行和第 11 行中两个解决方案的功能表达式不同,但是它们表示的功能相同。
注意
有关如何使用这些技术以及编写自己的符号求解器的更多信息,请参考官方 SymPy 页面上的出色文档,网址为[,网址为 http://docs.sympy.org/dev/modules/solvers/ode.html 。
解析*似法
解析*似方法尝试以基函数系统上的截断级数展开的形式来计算适用域上精确解的*似值。 在 SciPy 堆栈中,我们通过模块sympy.mpmath中的例程odefun实现了基于泰勒级数的实现。
注意
mpmath是一个用于任意精度浮点算术的 Python 库,位于sympy模块内部。 尽管它独立于numpy机制,但它们两者都能很好地协同工作。
有关此库的更多信息,请阅读位于 http://mpmath.org/doc/current/ 的官方文档。
让我们来看一下它的作用,首先举一个简单的例子 y'(t)= y(t), y(0)= 1 。 此处的关键是与区间 [0,1] 中的实际解相比,评估*似的速度和准确性。 它的语法非常简单,我们假定方程始终为 y'= F 的形式,并向例程odefun提供此函数 F 和初始条件( 在这种情况下,t 值 0 ,y 值 1 :
In [1]: import numpy as np, matplotlib.pyplot as plt; \
...: from sympy import mpmath
In [2]: def F(t, y): return y
In [3]: f = odefun(F, 0, 1)
我们将比较求解器f的结果和实际的解析解决方案np.exp:
In [4]: t = np.linspace(0, 1, 1024); \
...: Y1 = np.vectorize(f)(t); \
...: Y2 = np.exp(t)
In [5]: (np.abs(Y1-Y2)).max()
Out[5]: mpf('2.2204460492503131e-16')
让我们来看第二个例子。 我们与 [1,2] 区间中的实际解决方案相比,评估了执行时间和*似精度:
In [6]: def F(t, y): return 3.0*y**0.75 - 6.0*y/t
In [7]: def g(t): return (3.0*t**2.5 + 7)**4.0/(10000.0*t**6.)
In [8]: f = mpmath.odefun(F, 1.0, 1.0)
In [9]: t = np.linspace(1, 2, 1024); \
...: Y1 = np.vectorize(f)(t); \
...: Y2 = np.vectorize(g)(t)
In [9]: (np.abs(Y1-Y2)).max()
Out[9]: mpf('5.5511151231257827e-16')
现在,让我们用 Latko-Volterra 系统解决该示例。 我们计算解决方案并将其绘制在 0 到 10 个时间单位的时间范围内:
In [10]: def F(t, y): return [y[0] - 0.1*y[0]*y[1],
....: 0.075*y[0]*y[1] - 1.5*y[1]]
In [11]: f = mpmath.odefun(F, 0.0, [10.0, 5.0])
In [12]: T = [10.0*x/1023\. for x in range(1024)]
....: X = [f(10.0*x/1023.)[0] for x in range(1024)]; \
....: Y = [f(10.0*x/1023.)[1] for x in range(1024)]
In [13]: plt.plot(T, X, 'r--', linewidth=2.0, label='predator'); \
....: plt.plot(T, Y, 'b-', linewidth=2.0, label='prey'); \
....: plt.legend(loc=9); \
....: plt.grid(); \
....: plt.show()
最后一条命令为我们提供了下图。 虚线表示捕食者相对于时间的数量,实线表示猎物。 首先请注意解决方案的周期性。 另外,请注意两个函数的行为-当捕食者的数量很多时,猎物的数量就会减少。 在那时,捕食者很难找到食物,捕食者的数量开始减少,而猎物的数量却开始上升:

离散变量方法
在离散变量方法中,我们关注的是找到解的*似值,但仅在域中的离散点集处。 这些点可以在求解之前预先确定,也可以作为集成的一部分动态生成,以更好地适应所涉及功能的特性。 当解决方案具有奇异性时,例如,一旦生成离散点集,我们可以通过简单的插值过程为解决方案计算出一个很好的解析*似值,这将特别有用。
对于离散变量方法,我们有两种模式:
- 一步法:仅根据前一点的信息来计算一个点的解的值。 该方案的经典指数是,例如,欧拉方法,改进的欧拉方法,任何二阶两阶段方法或任何 Runge-Kutta 方法。
- 多步方法:某一点的求解值取决于先前几个要点的知识。 此类中最著名的算法是 Adams-Bashford 方法,Adams-Moulton 方法,任何后向差分方法或任何预测校正方法。
在模块scipy.integrate中,我们有公共接口类ode,它将使用所选的数值方法对方程/系统的解进行*似计算。 与我们以前使用的类相比,使用此类的方法大不相同,并且应该仔细解释一下:
- 一旦产生了微分方程/系统的右手边,例如说 y'= f(t,y),则该过程从创建求解器实例开始。 我们通过发出
ode(f)来实现。 如果我们对 y 变量描述了右侧的雅可比行列式,则可以将其包含在求解器ode(f, jac)的创建中。 - 如果需要向函数
f或它的 Jacobian 馈送额外的参数,则分别使用.set_f_params(*args)或.set_jac_params(*args)。 - 问题的初始值由属性
.set_initial_value(y[, t])指示。 - 现在是时候选择一种数值方案了。 我们通过设置
.set_integrator(name, **params)属性来实现。 如有必要,我们可以使用可选参数为所选方法提供更多信息。 - 最后,我们计算初始值问题的实际解决方案。 我们通常通过在循环中使用几个属性来完成此操作:
.integrate(t[, step, relax])将在提供的时间t上计算解y(t)的值。- 始终可以使用
attributes.t(用于时间变量)和.y(用于解决方案)来获取计算中的最后一步 - 为了检查计算是否成功,我们具有属性
.successful()。 - 一些集成方法接受一个标志函数
solout_func(t, y),该函数在每个成功步骤之后都会被调用。 这是通过.set_solout(solout_func)完成的。
一站式方法
SciPy 堆栈中编码的唯一一步方法是 Runge-Kutta 的两种实现,这是由 Dormand 和 Prince 设计的,由 Hairer 和 Wanner 为模块scipy.integrate编写的:
- 阶(4)的显式 Runge-Kutta 方法 5。 我们使用
method='dopri5'访问它。 - 阶 8(5,3)的显式 Runge-Kutta 方法。 我们称它为
method='dop853'。
让我们来看一下示例。 对于第一个,我们将通过在间隔 [0, 1] 中由 Chebyshev 多项式的零给出的 10 个节点上发出dopri5来求解微分方程。 :
In [1]: import numpy as np,import matplotlib.pyplot as plt; \
...: from scipy.integrate import ode; \
...: from scipy.special import t_roots
In [2]: def F(t, y): return y
In [3]: solver = ode(F) # solver created
In [4]: solver.set_initial_value(1.0, 0.0) # y(0) = 1
Out[4]: <scipy.integrate._ode.ode at 0x1038d3a50>
In [5]: solver.set_integrator('dopri5')
Out[5]: <scipy.integrate._ode.ode at 0x1038d3a50>
In [6]: solver.t, solver.y
Out[6]: (0.0, array([ 1.]))
In [7]: nodes = t_roots(10)[0]; \
...: nodes = (nodes + 1.0) * 0.5
In [8]: for k in range(10):
...: if solver.successful():
...: t = nodes[k]
...: solver.integrate(t)
...: print "{0},{1},{2}".format(t, solver.y[0], np.exp(t))
...:
0.00615582970243, 1.00617481576, 1.00617481576
0.0544967379058, 1.05600903161, 1.05600903161
0.146446609407, 1.15771311835, 1.15771311818
0.27300475013, 1.31390648743, 1.31390648604
0.42178276748, 1.52467728436, 1.52467727922
0.57821723252, 1.78285718759, 1.78285717608
0.72699524987, 2.06885488518, 2.06885486703
0.853553390593, 2.34797534217, 2.34797531943
0.945503262094, 2.57410852921, 2.5741085039
0.993844170298, 2.7015999731, 2.70159994653
注意
通过提供不同的容差,步数限制和其他稳定常数,可以对算法进行微调。 有关不同参数的详细说明,请参考上的官方文档,网址为 http://docs.scipy.org/doc/scipy/reference/generated/scipy.integrate.ode.html ,或仅要求 使用以下代码从 Python 会话中获取手册页:
>>> help(ode)
在伯努利方程的示例中,我们将再次收集 Chebyshev 多项式的根作为节点,但是这次我们将收集解并构造分段多项式插值,以将结果与真实解进行直观比较。 在这种情况下,我们采用 8(5,3)阶的 Runge-Kutta:
In [9]: def bernoulli(t, y): return 3*y**(0.75) - 6.0*y/t
In [10]: def G(t):
....: return (3.0*t**(2.5) + 7.0)**4.0 / (10000.0*t**6.0)
In [11]: solver = ode(bernoulli); \
....: solver.set_initial_value(1.0, 1.0); \
....: solver.set_integrator('dop853')
Out[11]: <scipy.integrate._ode.ode at 0x104667f50>
In [12]: T = np.linspace(1, 2, 1024); \
....: nodes = t_roots(10)[0]; \
....: nodes = 1.5 + 0.5 * nodes; \
....: solution = []
In [13]: for k in range(10):
....: if solver.successful():
....: solver.integrate(nodes[k])
....: solution += [solver.y[0]]
....:
In [14]: from scipy.interpolate import PchipInterpolator
In [15]: interpolant = PchipInterpolator(nodes, solution)
In [16]: plt.plot(T, interpolant(T), 'r--',
....: linewidth=2.0, label='approx.'); \
....: plt.plot(T, G(T), 'b-', label='true soln.'); \
....: plt.grid(); \
....: plt.legend(); \
....: plt.show()
这提供了以下启发图。 计算出的解(虚线)非常类似于时间t=1和t=2之间的真实解(实线):

Lotka-Volterra 系统以相同的方式求解。 在下面的示例中,我们将在一个周期中选择一组等距节点-从 0 到2π/√1.5的间隔(大约 5.13):
In [16]: def volterra(t, y):
....: return [y[0] - 0.1*y[0]*y[1],
....: 0.075*y[0]*y[1] - 1.5*y[1]]
In [17]: solver = ode(volterra); \
....: solver.set_initial_value([10.0, 5.0], 0.0); \
....: solver.set_integrator('dop853')
Out[17]: <scipy.integrate._ode.ode at 0x10461e390>
In [18]: prey = []; \
....: predator = []
In [19]: while (solver.t < 5.13 and solver.successful()):
....: solver.integrate(solver.t + 0.01)
....: prey += [solver.y[0]]
....: predator += [solver.y[1]]
....:
In [20]: plt.plot(prey, predator); \
....: plt.grid(); \
....: plt.xlabel('number of prey'); \
....: plt.ylabel('number of predators'); \
....: plt.show()
这为我们提供了系统的相图。 该图表示一条曲线,其中,对于每个时间单位, x 坐标表示猎物的数量,而相应的 y 坐标表示捕食者的数量。 由于解的周期性,相像是闭合曲线。 就像两种解决方案的简单图解一样,相像也说明了例如当捕食者的数量增加超过 20 个单位时,捕食者的数量通常很少(少于 20 个单位)。 当捕食者的数量降至 20 以下时,猎物的数量缓慢增加至 40 多个单位:

两步法
在这里,我们有两个不同的选择:一个 Adams-Moulton 方法(适用于非刚度方程)和一个后向差分方法(为刚度方程设计)。 对于这两种数值方法中的每一种,我们都有两种不同的实现方式,具体取决于用于计算解的背景 Fortran 例程。 选项如下:
-
*VODE:在 Fortran 库 ODE 中,我们有例程VODE和ZVODE(分别用于初值问题的实值和复值解)。 例如,对于实值问题,要访问 Adams-Moulton 的数值方法,我们发布属性.set_integrator('vode', method='adams')。 为了访问后向差异,我们发出.set_integrator('vode', method='BDF')。 -
LSODA: This other implementation wraps different routines from the Fortran libraryODEPACK. The calls are exactly as in the previous case, substituting'vode'or'zvode'with'lsoda'instead.注意
有关 netlib 库
ODE和ODEPACK的更多信息,请参考 http://www.netlib.org/ode/ 和 http://www.netlib.org/odepack / 。
这些数值方法是为大问题设计的。 对于较小的任务(具有少量节点的非刚性一维方程式),应改用 Runge-Kutta。 第二个示例说明了这一点:我们应用VODE中的BDF,并将之前从两个 Runge-Kutta 获得的解与实际解进行比较。 请注意,在这种简单情况下,dop853如何胜过BDF:
In [21]: solver = ode(bernoulli); \
....: solver.set_initial_value(1.0, 1.0); \
....: solver.set_integrator('vode', method='BDF')
Out[21]: <scipy.integrate._ode.ode at 0x1038d2990>
In [22]: nodes = t_roots(10)[0]; \
....: nodes = 1.5 + 0.5 * nodes; \
....: solution2 = []
In [23]: for k in range(10):
....: if solver.successful():
....: solver.integrate(nodes[k])
....: solution2 += [solver.y[0]]
....:
In [24]: for k in range(10):
....: true = G(nodes[k])
....: dop853 = solution[k]
....: vode = solution2[k]
....: print "{0},{1},{2}".format(true, dop853, vode)
....:
0.981854827818, 0.981854827818, 0.981855789349
0.859270248468, 0.859270248468, 0.859270080689
0.698456572663, 0.698456572663, 0.698458875953
0.570963875566, 0.570963875559, 0.57096210196
0.49654019444, 0.496540194433, 0.496537599383
0.466684337536, 0.466684337531, 0.466681706374
0.466700776675, 0.46670077667, 0.466699552511
0.482536531899, 0.482536531895, 0.482537207918
0.501698475572, 0.501698475568, 0.501699366611
0.514159743133, 0.514159743128, 0.514160809167
为了完成本章,我们在 Lotka-Volterra 系统上使用LSODA,包括 Jacobian 信息:
In [25]: def jacF(t, y):
....: output = np.zeros((2,2))
....: output[0,0] = 1.0 - 0.1*y[1]
....: output[0,1] = -0.1*y[0]
....: output[1,0] = 0.075*y[1]
....: output[1,1] = 0.075*y[0] - 1.5
....: return output
In [26]: solver = ode(volterra, jacF); \
....: solver.set_initial_value([10.0, 5.0], 0.0); \
....: solver.set_integrator('lsoda', method='adams',
....: with_jacobian=True)
In [27]: prey2 = []; \
....: predator2 = []
In [28]: while (solver.t < 5.13 and solver.successful()):
....: solver.integrate(solver.t + 0.01)
....: prey2 += [solver.y[0]]
....: predator2 += [solver.y[1]]
我们将其作为练习,以比较使用此方法解决此最后一个系统的结果与先前的 Runge-Kutta 过程。
摘要
在这一简短的章节中,我们已经掌握了所有符号和数值技术,可以解决微分方程/系统和相关的初值问题。
在下一章中,我们将探索 SciPy 堆栈中拥有哪些资源,以解决计算几何中的问题。
六、计算几何
“计算几何”是一个数学领域,致力于开发有效的算法来解决根据基本几何对象描述的问题。 我们区分组合计算几何和数值计算几何。
组合计算几何处理基本几何对象(点,线段,线,多边形和多面体)的相互作用。 在这种情况下,我们有三类问题:
- 静态问题:需要从一组输入几何对象中构造一个已知目标对象
- 几何查询问题:给定一组已知对象(搜索空间)和一个查找的属性(查询),这些问题将处理满足查询条件的对象的搜索
- 动态问题:这与前两个类别中的问题相似,但又增加了挑战,即事先不知道输入,并且在查询/构造之间插入或删除对象
数值计算几何主要处理空间中对象的表示,这些对象通过曲线,曲面和由其限定的空间中的区域来描述。
在我们继续开发和分析这两种设置中的不同算法之前,有必要探索一下基本背景-*面几何。
*面几何
SymPy 库的几何模块涵盖了基本的几何功能。 我们没有给出该模块中所有对象和属性的学术描述,而是通过一系列小型的不言自明的 Python 会话来发现最有用的对象和属性。
我们从点和段的概念开始。 目的是举例说明我们如何容易地检查共线性,计算长度,中点或线段的斜率。 我们还将展示如何快速计算两个线段之间的角度,以及如何确定给定点是否属于线段。 下图说明了一个示例,我们将继续执行代码:

In [1]: from sympy.geometry import Point, Segment, Line, \
...: Circle, Triangle, Curve
In [2]: P1 = Point(0, 0); \
...: P2 = Point(3, 4); \
...: P3 = Point(2, -1); \
...: P4 = Point(-1, 5)
In [3]: statement = Point.is_collinear(P1, P2, P3); \
...: print "Are P1, P2, P3 collinear?," statement
Are P1, P2, P3 collinear? False
In [4]: S1 = Segment(P1, P2); \
...: S2 = Segment(P3, P4)
In [5]: print "Length of S1:", S1.length
Length of S1: 5
In [6]: print "Midpoint of S2:", S2.midpoint
Midpoint of S2: Point(1/2, 2)
In [7]: print "Slope of S1", S1.slope
Slope of S1: 4/3
In [8]: print "Intersection of S1 and S2:", S1.intersection(S2)
Intersection of S1 and S2: [Point(9/10, 6/5)]
In [9]: print "Angle between S1, S2:", Segment.angle_between(S1, S2)
Angle between S1, S2: acos(-sqrt(5)/5)
In [10]: print "Does S1 contain P3?", S1.contains(P3)
Does S1 contain P3? False
下一个逻辑几何概念是线。 我们可以使用行执行更多有趣的操作,并且为此,我们还有更多的构造函数。 我们可以找到他们的方程式; 计算点与线之间的距离以及许多其他操作:

In [11]: L1 = Line(P1, P2)
In [12]: L2 = L1.perpendicular_line(P3) #perpendicular line to L1
In [13]: print "Parametric equation of L2:", L2.arbitrary_point()
Parametric equation of L2: Point(4*t + 2, -3*t – 1)
In [14]: print "Algebraic equation of L2:", L2.equation()
Algebraic equation of L2: 3*x + 4*y - 2
In [15]: print "Does L2 contain P4?", L2.contains(P4)
Does L2 contain P4? False
In [16]: print "Distance from P4 to L2:", L2.distance(P4)
Distance from P4 to L2: 3
In [17]: print "Is L2 parallel with S2?", L1.is_parallel(S2)
Is L2 parallel with S2? False
我们要探讨的下一个几何概念是圆。 我们可以通过圆的中心和半径或圆上的三个点来定义一个圆。 我们可以轻松计算其所有属性,如下图所示:

In [18]: C1 = Circle(P1, 3); \
....: C2 = Circle(P2, P3, P4)
In [19]: print "Area of C2:", C2.area
Area of C2: 1105*pi/98
In [20]: print "Radius of C2:", C2.radius
Radius of C2: sqrt(2210)/14
In [21]: print "Algebraic equation of C2:", C2.equation()
Algebraic equation of C2: (x - 5/14)**2 + (y - 27/14)**2 - 1105/98
In [22]: print "Center of C2:", C2.center
Center of C2: Point(5/14, 27/14)
In [23]: print "Circumference of C2:", C2.circumference
Circumference of C2: sqrt(2210)*pi/7
计算与其他对象的相交,检查一条线是否与圆相切或找到通过外部点的切线也很简单:
In [24]: print "Intersection of C1 and C2:\n", C2.intersection(C1)
Intersection of C1 and C2:
[Point(55/754 + 27*sqrt(6665)/754, -5*sqrt(6665)/754 + 297/754),
Point(-27*sqrt(6665)/754 + 55/754, 297/754 + 5*sqrt(6665)/754)]
In [25]: print "Intersection of S1 and C2:\n", C2.intersection(S1)
Intersection of S1 and C2:
[Point(3, 4)]
In [26]: print "Is L2 tangent to C2?", C2.is_tangent(L2)
Is L2 tangent to C2? False
In [27]: print "Tangent lines to C1 through P4:\n", \
C1.tangent_lines(P4)
Tangent lines to C1 through P4:
[Line(Point(-1, 5),
Point(-9/26 + 15*sqrt(17)/26, 3*sqrt(17)/26 + 45/26)),
Line(Point(-1, 5),
Point(-15*sqrt(17)/26 - 9/26, -3*sqrt(17)/26 + 45/26))]
三角形是一个非常有用的基本几何概念。 这些对象的可靠处理是计算几何的核心。 我们需要强大而快速的算法来处理和提取信息。 让我们首先显示一个的定义,以及描述其属性的一系列查询:
In [28]: T = Triangle(P1, P2, P3)
In [29]: print "Signed area of T:", T.area
Signed area of T: -11/2
In [30]: print "Angles of T:\n", T.angles
Angles of T:
{Point(3, 4): acos(23*sqrt(26)/130),
Point(2, -1): acos(3*sqrt(130)/130),
Point(0, 0): acos(2*sqrt(5)/25)}
In [31]: print "Sides of T:\n", T.sides
Sides of T:
[Segment(Point(0, 0), Point(3, 4)),
Segment(Point(2, -1), Point(3, 4)),
Segment(Point(0, 0), Point(2, -1))]
In [32]: print "Perimeter of T:", T.perimeter
Perimeter of T: sqrt(5) + 5 + sqrt(26)
In [33]: print "Is T a right triangle?", T.is_right()
Is T a right triangle? False
In [34]: print "Is T equilateral?", T.is_equilateral()
Is T equilateral? False
In [35]: print "Is T scalene?", T.is_scalene()
Is T scalene? True
In [36]: print "Is T isosceles?", T.is_isosceles()
Is T isosceles? False
接下来,请注意我们如何轻松地获得与三角形以及中间三角形(在顶点的中点处具有顶点的三角形)关联的不同线段,中心和圆的表示形式:
In [37]: T.altitudes
Out[37]:
{Point(0, 0) : Segment(Point(0, 0), Point(55/26, -11/26)),
Point(2, -1): Segment(Point(6/25, 8/25), Point(2, -1)),
Point(3, 4) : Segment(Point(4/5, -2/5), Point(3, 4))}
In [38]: T.orthocenter # Intersection of the altitudes
Out[38]:
Point((3*sqrt(5) + 10)/(sqrt(5) + 5 + sqrt(26)), (-5 + 4*sqrt(5))/(sqrt(5) + 5 + sqrt(26)))
In [39]: T.bisectors() # Angle bisectors
Out[39]:
{Point(0, 0) : Segment(Point(0, 0), Point(sqrt(5)/4 + 7/4, -9/4 + 5*sqrt(5)/4)),
Point(2, -1): Segment(Point(3*sqrt(5)/(sqrt(5) + sqrt(26)), 4*sqrt(5)/(sqrt(5) + sqrt(26))),
Point(2, -1)),
Point(3, 4) : Segment(Point(-50 + 10*sqrt(26), -5*sqrt(26) + 25), Point(3, 4))}
In [40]: T.incenter # Intersection of angle bisectors
Out[40]:
Point((3*sqrt(5) + 10)/(sqrt(5) + 5 + sqrt(26)), (-5 + 4*sqrt(5))/(sqrt(5) + 5 + sqrt(26)))
In [41]: T.incircle
Out[41]:
Circle(Point((3*sqrt(5) + 10)/(sqrt(5) + 5 + sqrt(26)),
(-5 + 4*sqrt(5))/(sqrt(5) + 5 + sqrt(26))),
-11/(sqrt(5) + 5 + sqrt(26)))
In [42]: T.inradius
Out[42]: -11/(sqrt(5) + 5 + sqrt(26))
In [43]: T.medians
Out[43]:
{Point(0, 0) : Segment(Point(0, 0), Point(5/2, 3/2)),
Point(2, -1): Segment(Point(3/2, 2), Point(2, -1)),
Point(3, 4) : Segment(Point(1, -1/2), Point(3, 4))}
In [44]: T.centroid # Intersection of the medians
Out[44]: Point(5/3, 1)
In [45]: T.circumcenter # Intersection of perpendicular bisectors
Out[45]: Point(45/22, 35/22)
In [46]: T.circumcircle
Out[46]: Circle(Point(45/22, 35/22), 5*sqrt(130)/22)
In [47]: T.circumradius
Out[47]: 5*sqrt(130)/22
In [48]: T.medial
Out[48]: Triangle(Point(3/2, 2), Point(5/2, 3/2), Point(1, -1/2))
以下是一些其他有趣的三角形运算:
- 与其他物体的相交
- 从点到每个线段的最小距离的计算
- 检查两个三角形是否相似
In [49]: T.intersection(C1)
Out[49]: [Point(9/5, 12/5), Point(sqrt(113)/26 + 55/26, -11/26 + 5*sqrt(113)/26)]
In [50]: T.distance(T.circumcenter)
Out[50]: sqrt(26)/11
In [51]: T.is_similar(Triangle(P1, P2, P4))
Out[51]: False
当前在几何模块中编码的其他基本几何对象是:
LinearEntity:这是具有三个子类的超类:Segment,Line和Ray。LinearEntity类具有以下基本方法:are_concurrent(o1, o2, ..., on)are_parallel(o1, o2)are_perpendicular(o1, o2)parallel_line(self, Point)perpendicular_line(self, Point)perpendicular_segment(self, Point)
Ellipse:这是一个具有中心以及水*和垂直半径的对象。Circle实际上是两个半径相等的Ellipse的子类。Polygon:这是一个超类,我们可以通过列出一组顶点来实例化。 例如,Triangles是Polygon的子类。 多边形的基本方法是:areaperimetercentroidsidesvertices
RegularPolygon。 这是Polygon的子类,具有额外的属性:-
apothem -
center -
circumcircle -
exterior_angle -
incircle -
interior_angle -
radius提示
有关此模块的更多信息,请参考官方的 SymPy 文档,网址为 http://docs.sympy.org/latest/modules/geometry/index.html 。
-
还有一个非基本几何对象-曲线,我们通过提供参数方程式以及参数定义的间隔来定义它。 除了描述其构造函数的方法外,它目前没有许多有用的方法。 让我们说明如何处理这些对象。 例如,椭圆的四分之三弧可以编码如下:
In [52]: from sympy import var, pi, sin, cos
In [53]: var('t', real=True)
In [54]: Arc = Curve((3*cos(t), 4*sin(t)), (t, 0, 3*pi/4))
要结束对 SymPy 库中来自 geometry 模块的基本对象的阐述,我们必须提到,我们可以将任何基本仿射变换应用于任何先前的对象。 这是通过组合reflect,rotate,translate和scale方法完成的:
In [55]: T.reflect(L1)
Out[55]: Triangle(Point(0, 0), Point(3, 4), Point(-38/25, 41/25))
In [56]: T.rotate(pi/2, P2)
Out[56]: Triangle(Point(7, 1), Point(3, 4), Point(8, 3))
In [57]: T.translate(5,4)
Out[57]: Triangle(Point(5, 4), Point(8, 8), Point(7, 3))
In [58]: T.scale(9)
Out[58]: Triangle(Point(0, 0), Point(27, 4), Point(18, -1))
In [59]: Arc.rotate(pi/2, P3).translate(pi,pi).scale(0.5)
Out[59]:
Curve((-2.0*sin(t) + 0.5 + 0.5*pi, 3*cos(t) - 3 + pi), (t, 0, 3*pi/4))
通过这些基本定义和操作,我们已准备好应对更复杂的情况。 接下来让我们探索这些新挑战。
组合计算几何
也称为算法几何,此领域的应用很多。 在机器人技术中,例如,它用于解决可见性问题和运动计划。 在地理信息系统( GIS )中,采用了类似的应用程序来设计路线规划或搜索算法。
让我们描述问题的不同类别,重点介绍解决问题的工具,这些工具在 SciPy 堆栈中可用。
静态问题
此类别中的基本问题如下:
-
凸包:给定空间中的一组点,找到包含它们的最小凸多面体。
-
Voronoi 图:给定空间中的一组点(种子),请计算由更接*每个种子的所有点组成的区域中的分区。
-
三角剖分:用两个三角形分开的方式将*面与三角形分开,否则它们将共享一条边或一个顶点。 根据输入对象或三角形属性的约束,有不同的三角剖分。
-
Shortest paths: Given a set of obstacles in a space and two points, find the shortest path between the points that does not intersect any of the obstacles.
提示
凸包,基本三角剖分和 Voronoi 图的计算问题紧密相关。 由 Franco Preparata 和 Michael Shamos 撰写的题为“计算几何”的计算机科学专着中详细解释了解释这个美丽主题的理论。 它由 Springer-Verlag 于 1985 年出版。
凸包
尽管可以通过库 SymPy 的几何模块计算*面中相当大的一组点的凸包,但不建议这样做。 可通过类ConvexHull在模块scipy.spatial中获得更快,更可靠的代码,该类可从Qhull库( http://www.qhull 实现对例程qconvex的包装)。 org / )。 该例程还允许以更高的尺寸计算凸包。 让我们将两种方法与著名的苏必利尔湖多边形superior.poly进行比较。
提示
多边形文件表示*面直线图-顶点和边的简单列表,以及在某些情况下有关孔和凹面的信息。 可以从 https://github.com/blancosilva/Mastering-Scipy/blob/master/chapter6/superior.poly 下载正在运行的示例。
这包含对苏必利尔湖海岸线的多边形描述,其中有 7 个孔(用于岛屿),518 个顶点和 518 个边。
有关多边形格式的完整说明,请参见 http://www.cs.cmu.edu/~quake/triangle.poly.html 。 有了这些信息,我们可以轻松编写一个简单的阅读器。
以下是一个示例。
# part of file chapter6.py
from numpy import array
def read_poly(file_name):
"""
Simple poly-file reader, that creates a python dictionary
with information about vertices, edges and holes.
It assumes that vertices have no attributes or boundary markers.
It assumes that edges have no boundary markers.
No regional attributes or area constraints are parsed.
"""
output = {'vertices': None, 'holes': None, 'segments': None}
# open file and store lines in a list
file = open(file_name, 'r')
lines = file.readlines()
file.close()
lines = [x.strip('\n').split() for x in lines]
# Store vertices
vertices= []
N_vertices,dimension,attr,bdry_markers = [int(x) for x in lines[0]]
# We assume attr = bdrt_markers = 0
for k in range(N_vertices):
label,x,y = [items for items in lines[k+1]]
vertices.append([float(x), float(y)])
output['vertices']=array(vertices)
# Store segments
segments = []
N_segments,bdry_markers = [int(x) for x in lines[N_vertices+1]]
for k in range(N_segments):
label,pointer_1,pointer_2 = [items for items in lines[N_vertices+k+2]]
segments.append([int(pointer_1)-1, int(pointer_2)-1])
output['segments'] = array(segments)
# Store holes
N_holes = int(lines[N_segments+N_vertices+2][0])
holes = []
for k in range(N_holes):
label,x,y = [items for items in lines[N_segments + N_vertices + 3 + k]]
holes.append([float(x), float(y)])
output['holes'] = array(holes)
return output
注意,将每个顶点加载为Point以及使用该结构计算凸包需要太多的资源和太多的时间。 注意区别:
In [1]: import numpy as np, matplotlib.pyplot as plt; \
...: from sympy.geometry import Point, convex_hull; \
...: from scipy.spatial import ConvexHull; \
...: from chapter6 import read_poly
In [2]: lake_superior = read_poly("superior.poly"); \
...: vertices_ls = lake_superior['vertices']
In [3]: %time hull = ConvexHull(vertices_ls)
CPU times: user 1.59 ms, sys: 372 µs, total: 1.96 ms
Wall time: 1.46 ms
In [4]: vertices_sympy = [Point(x) for x in vertices_ls]
In [5]: %time convex_hull(*vertices_sympy)
CPU times: user 168 ms, sys: 54.5 ms, total: 223 ms
Wall time: 180 ms
Out[5]:
Polygon(Point(1/10, -629607/1000000), Point(102293/1000000, -635353/1000000),
Point(2773/25000, -643967/1000000), Point(222987/1000000, -665233/1000000),
Point(8283/12500, -34727/50000), Point(886787/1000000, -1373/2000),
Point(890227/1000000, -6819/10000), Point(9/10, -30819/50000),
Point(842533/1000000, -458913/1000000), Point(683333/1000000, -17141/50000),
Point(16911/25000, -340427/1000000), Point(654027/1000000, -333047/1000000),
Point(522413/1000000, -15273/50000), Point(498853/1000000, -307193/1000000),
Point(5977/25000, -25733/50000), Point(273/2500, -619833/1000000))
让我们使用scipy.spatial.ConvexHull的计算来生成带有解决方案的图表:
提示
使用简单命令convex_hull_plot_2d,也可以在二维中绘制一组顶点及其凸包(一次使用ConvexHull计算)。 它需要matplotlib.pyplot。
In [5]: plt.figure(); \
...: plt.xlim(vertices_ls[:,0].min()-0.01, vertices_ls[:,0].max()+0.01); \
...: plt.ylim(vertices_ls[:,1].min()-0.01, vertices_ls[:,1].max()+0.01); \
...: plt.axis('off'); \
...: plt.axes().set_aspect('equal'); \
...: plt.plot(vertices_ls[:,0], vertices_ls[:,1], 'b.')
Out[5]: [<matplotlib.lines.Line2D at 0x10ee3ab10>]
In [6]: for simplex in hull.simplices:
...: plt.plot(vertices_ls[simplex, 0], ...: vertices_ls[simplex, 1], 'r-')
In [7]: plt.show()
这将绘制以下图像:

要修改ConvexHull的输出,我们可以通过参数qhull_options传递所有必需的qconvex控件。 有关所有qconvex控件和其他输出选项的列表,请参阅 Qhull 手册,网址为,网址为 http://www.qhull.org/html/index.htm 。 在本章中,我们满意地显示了如果点的尺寸大于 4,则使用默认控件qhull_options='Qx Qt'获得的结果,否则,将显示qhull_options='Qt'。
现在让我们说明ConvexHull的一些高级用法。 首先,计算 3D 空间中随机点集的凸包。 为了可视化,我们将使用mayavi库:
In [8]: points = np.random.rand(320, 3)
In [9]: hull = ConvexHull(points)
In [10]: X = hull.points[:, 0]; \
....: Y = hull.points[:, 1]; \
....: Z = hull.points[:, 2]
In [11]: from mayavi import mlab
In [12]: mlab.triangular_mesh(X, Y, X, hull.simplices,
....: colormap='gray', opacity=0.5,
....: representation='wireframe')
这将绘制以下图像:

Voronoi 图
可以使用模块scipy.spatial中的类Voronoi(及其用于可视化的同伴voronoi_plot_2d)来计算一组顶点(我们的种子)的 Voronoi 图。 此类从Qhull库实现例程qvoronoi的包装,如果点的尺寸大于 4,则使用以下默认控件qhull_option='Qbb Qc Qz Qx',否则使用qhull_options='Qbb Qc Qz'。 为了计算最远站点的 Voronoi 图,而不是最*站点,我们将添加额外的控件'Qu'。
让我们用普通的 Voronoi 图来做一个简单的例子:
In [13]: from scipy.spatial import Voronoi, voronoi_plot_2d
In [14]: vor = Voronoi(vertices_ls)
为了理解输出,将通过限制voronoi_plot_2d获得的可视化效果在一个小窗口中复制我们获得的图表非常有说明性,该窗口位于苏必利尔湖北岸的某个中心:
In [15]: ax = plt.subplot(111, aspect='equal'); \
....: voronoi_plot_2d(vor, ax=ax); \
....: plt.xlim( 0.45, 0.50); \
....: plt.ylim(-0.40, -0.35); \
....: plt.show()
这将绘制以下图像:

-
小点是 x 坐标在
0.45和0.50之间, y 坐标在-0.40和-0.35之间的原始种子。 我们可以从原始列表vertices_ls或vor.points中访问这些值。 -
*面被分成不同的区域(Voronoi 细胞),每个种子一个。 这些区域包含*面中最接*其种子的所有点。 每个区域都接收一个索引,该索引不一定与
vor.points列表中其种子的索引相同。 要访问给定种子的相应区域,我们使用vor.point_region:In [16]: vor.point_region Out[16]: array([ 0, 22, 24, 21, 92, 89, 91, 98, 97, 26, 218, 219, 220, 217, 336, 224, 334, 332, 335, 324, 226, 231, 230, 453, 500, 454, 235, 234, 333, 236, 341, 340, 93, ... 199, 81, 18, 17, 205, 290, 77, 503, 469, 473, 443, 373, 376, 366, 370, 369, 210, 251, 367, 368, 377, 472, 504, 506, 502, 354, 353, 54, 42, 43, 350, 417, 414, 415, 418, 419, 425]) -
每个 Voronoi 单元格均由其定界顶点和边缘(在 Voronoi 行话中也称为脊)定义。 可以使用
vor.vertices获得具有 Voronoi 图的计算顶点坐标的列表。 这些顶点在上一张图像中被表示为较大的点,并且由于它们始终位于至少两个边缘的交点处而种子没有输入边缘,因此易于识别:In [17]: vor.vertices Out[17]: array([[ 0.88382749, -0.23508215], [ 0.10607886, -0.63051169], [ 0.03091439, -0.55536174], ..., [ 0.49834202, -0.62265786], [ 0.50247159, -0.61971784], [ 0.5028735 , -0.62003065]]) -
For each of the regions, we can access the set of delimiting vertices with
vor.regions. For instance, to obtain the coordinates of the vertices that delimit the region around the fourth seed, we could issue the following command:In [18]: [vor.vertices[x] for x in vor.regions[vor.point_region[4]]] Out[18]: [array([ 0.13930793, -0.81205929]), array([ 0.11638 , -0.92111088]), array([ 0.11638 , -0.63657789]), array([ 0.11862537, -0.6303235 ]), array([ 0.12364332, -0.62893576]), array([ 0.12405738, -0.62891987])]必须注意上一步-Voronoi 单元的某些顶点不是实际顶点,而是位于无穷大处。 在这种情况下,它们使用索引
-1进行标识。 在这种情况下,为了准确表示具有这些特征的山脊,我们必须使用两个种子的知识,它们的连续 Voronoi 细胞在所述山脊上相交-因为山脊垂直于由这两个种子定义的部分。 我们使用vor.ridge_points获得有关这些种子的信息:In [19]: vor.ridge_points Out[19]: array([[ 0, 1], [ 0, 433], [ 0, 434], ..., [124, 118], [118, 119], [119, 122]])可以将
vor.ridge_points的第一项解读为,有一条垂直于第一种子和第二种子的脊。
我们可以使用对象vor的其他属性来查询 Voronoi 图的属性,但是我们所描述的属性应该足以复制先前的图。 我们将其作为一个不错的练习:
- 收集
vor.points中具有 x 坐标和 y 坐标的种子的索引。 绘制它们。 - 对于这些种子中的每一个,收集有关其相应 Voronoi 细胞顶点的信息。 绘制那些非无限顶点的样式与种子不同。
- 收集有关每个相关区域的山脊的信息,并将它们绘制为简单的细段。 有些山脊无法用其两个顶点表示。 在这种情况下,我们将使用有关确定种子的信息。
三角剖分
*面中一组顶点的三角剖分是将顶点的凸包分割成三角形,满足一个重要条件。 给定的两个三角形可以是以下任意一个:
- 他们必须脱节
- 它们只能在一个共同的顶点相交
- 他们必须有一个共同的优势
这些简单的三角剖分没有太大的计算价值,因为它们的某些三角形可能太瘦了—这会导致令人不舒服的舍入误差,计算或错误的区域,中心等。 在所有可能的三角剖分中,我们总是寻求一种三角形的属性以某种方式*衡的三角剖分。
考虑到这一目的,我们对一组顶点进行了 Delaunay 三角剖分。 此三角剖分满足一个额外的条件-顶点中没有一个位于任何三角形外接圆的内部。 我们将具有此属性的三角形称为 Delaunay 三角形。
对于此更简单的设置,在模块scipy.spatial中,我们具有类Delaunay,该类实现了对Qhull库中的例程qdelaunay的包装,并且控件的设置与 Voronoi 相同 图表:
In [20]: from scipy.spatial import Delaunay
In [21]: tri = Delaunay(vertices_ls)
In [22]: plt.figure()
....: plt.xlim(vertices_ls[:,0].min()-0.01, vertices_ls[:,0].max()+0.01); \
....: plt.ylim(vertices_ls[:,1].min()-0.01,vertices_ls[:,1].max()+0.01); \
....: plt.axes().set_aspect('equal'); \
....: plt.axis('off'); \
....: plt.triplot(vertices_ls[:,0], vertices_ls[:,1], tri.simplices, 'k-'); \
....: plt.plot(vertices_ls[:,0], vertices_ls[:,1], 'r.'); \
....: plt.show()
绘制下图:

也可以生成带有强加边缘的三角剖分。 给定顶点和边缘的集合,将约束 Delaunay 三角剖分是将空间划分为具有这些指定特征的三角形。 此三角剖分中的三角形不一定是 Delaunay。
我们有时可以通过细分每个施加的边来完成此额外条件。 我们称这种三角剖分为 Delaunay ,将细分边缘所需的新(人工)顶点称为 Steiner 点。
施加的一组顶点和边的约束顺应 Delaunay 三角剖分满足更多条件,通常在三角形的角度或面积值上设置阈值。 这可以通过引入一组新的 Steiner 点来实现,这些点不仅可以在边缘上使用,而且可以在任何地方使用。
提示
为了实现这些高级三角剖分,我们需要离开 SciPy 堆栈。 我们有一个 Python 封装程序,用于实现网格生成器的惊人实现triangle,作者是 Richard Shewchuck( http://www.cs.cmu.edu/~quake/triangle.html )。 可以通过发出easy_install triangle或pip install triangle从提示安装此包装,以及示例和其他相关功能。 有关此模块的更多信息,请参考其作者 Dzhelil Rufat 的在线文档,网址为,网址为 http://dzhelil.info/triangle/index.html 。
让我们为正在运行的示例计算那些不同的三角剖分。 我们再次将 poly 文件与苏必利尔湖的特征一起使用,将其读入包含有关顶点,线段和孔的所有信息的字典中。 第一个示例是约束 Delaunay 三角剖分(cndt)的示例。 我们用标志p(表明源是*面直线图,而不是一组顶点)完成此任务:
In [23]: from triangle import triangulate, plot as tplot
In [24]: cndt = triangulate(lake_superior, 'p')
In [25]: ax = plt.subplot(111, aspect='equal'); \
....: tplot.plot(ax, **cndt); \
....: plt.show()
请注意,相对于先前的图表而言有所改进,并且原始多边形之外没有三角形:

下一步是计算符合的 Delaunay 三角剖分(cfdt)。 我们在某些线段上施加 Steiner 点,以确保尽可能多的 Delaunay 三角形。 我们通过额外的D标志来实现:
In [26]: cfdt = triangulate(lake_superior, 'pD')
在这种情况下,可以观察到相对于先前的图略有改善或无改善。 当我们进一步对三角形的最小角度值(带有标记q)或三角形的面积的最大值(带有标记a)施加约束时,就会出现真正的改进。 例如,如果我们需要一个约束一致的 Delaunay 三角剖分(cncfdt),其中所有三角形的最小角度至少为 20 度,则发出以下命令:
In [27]: cncfq20dt = triangulate(lake_superior, 'pq20D')
In [28]: ax = plt.subplot(111, aspect='equal'); \
....: tplot.plot(ax, **cncfq20dt); \
....: plt.show()
如下图所示,这为我们提供了更好的结果:

作为本节的总结,我们提供了最后一个示例,其中我们进一步在三角形上施加了最大面积:
In [29]: cncfq20adt = triangulate(lake_superior, 'pq20a.001D')
In [30]: ax = plt.subplot(111, aspect='equal'); \
....: tplot.plot(ax, **cncfq20adt); \
....: plt.show()
最后一个(非常令人满意的)图如下:

最短路径
我们将使用前面的示例为最短路径问题引入特殊设置。 我们在湖的西北海岸选择一个位置(例如,在原始多边形文件中索引为 370 的顶点),目标是在右下角计算到海岸上最东南位置的最短路径 —这是原始多边形文件中索引为 179 的顶点。 在这种情况下,通过路径表示三角形的边缘链。
在 SciPy 堆栈中,我们依靠两个模块来完成三角剖分中最短路径的计算(以及一些其他可以通过图形编码的相似几何图形):
-
scipy.sparse用于存储表示三角剖分的加权邻接矩阵G。 该邻接矩阵的每个非零条目G[i,j]恰好是从顶点i到顶点j的边的长度。 -
scipy.sparse.csgraphis the module that deals with compressed sparse graphs. This module contains routines to analyze, extract information, or manipulate graphs. Among these routines, we have several different algorithms to compute the shortest paths on a graph.提示
有关模块
scipy.sparse.csgraph的更多信息,请参考位于 http://docs.scipy.org/doc/scipy/reference/sparse.csgraph.html 的在线文档。对于图论的理论和应用,最好的资料之一是 Reinhard Diestel 的入门书籍图论,由 Springer-Verlag 出版。
让我们用适当的代码说明此示例。 我们首先收集三角剖分中所有线段的顶点的索引以及这些线段的长度。
提示
为了计算每个段的长度,而不是从头开始创建一个例程,该例程将可靠的范数函数应用到相关顶点的两个列表的差的每一项上,我们使用模块scipy.spatial中的minkowski_distance。
In [31]: X = cncfq20adt['triangles'][:,0]; \
....: Y = cncfq20adt['triangles'][:,1]; \
....: Z = cncfq20adt['triangles'][:,2]
In [32]: Xvert = [cncfq20adt['vertices'][x] for x in X]; \
....: Yvert = [cncfq20adt['vertices'][y] for y in Y]; \
....: Zvert = [cncfq20adt['vertices'][z] for z in Z]
In [33]: from scipy.spatial import minkowski_distance
In [34]: lengthsXY = minkowski_distance(Xvert, Yvert); \
....: lengthsXZ = minkowski_distance(Xvert, Zvert); \
....: lengthsYZ = minkowski_distance(Yvert, Zvert)
现在,我们创建加权邻接矩阵,将其存储为lil_matrix,并计算所请求顶点之间的最短路径。 我们在列表中收集计算路径中包括的所有顶点,并绘制覆盖在三角剖分上的结果链。
提示
警告:
我们将要计算的邻接矩阵不是距离矩阵。 在距离矩阵A中,我们在每个条目A[i, j]上包括任何顶点i与任何顶点j之间的距离,而不管是否通过边连接。 如果需要此距离矩阵,最可靠的计算方法是通过模块scipy.spatial中的例程distance_matrix:
>>> from scipy.spatial import distance_matrix
>>> A = distance_matrix(cncfq20adt['vertices'], cncfq20adt['vertices'])
In [35]: from scipy.sparse import lil_matrix; \
....: from scipy.sparse.csgraph import shortest_path
In [36]: nvert = len(cncfq20adt['vertices']); \
....: G = lil_matrix((nvert, nvert))
In [37]: for k in range(len(X)):
....: G[X[k], Y[k]] = G[Y[k], X[k]] = lengthsXY[k]
....: G[X[k], Z[k]] = G[Z[k], X[k]] = lengthsXZ[k]
....: G[Y[k], Z[k]] = G[Z[k], Y[k]] = lengthsYZ[k]
....:
In [38]: dist_mat, pred = shortest_path(G, return_predecessors=True,
....: directed=True, ....: unweighted=False)
In [39]: index = 370; \
....: path = [370]
In [40]: while index != 197:
....: index = pred[197, index]
....: path.append(index)
....:
In [41]: print path
[380, 379, 549, 702, 551, 628, 467, 468, 469, 470, 632, 744, 764, 799, 800, 791, 790, 789, 801, 732, 725, 570, 647, 177, 178, 179, 180, 181, 182, 644, 571, 201, 200, 199, 197]
In [42]: ax = plt.subplot(111, aspect='equal'); \
....: tplot.plot(ax, **cncfq20adt)
In [43]: Xs = [cncfq20adt['vertices'][x][0] for x in path]; \
....: Ys = [cncfq20adt['vertices'][x][1] for x in path]
In [44]: ax.plot(Xs, Ys '-', linewidth=5, color='blue'); \
....: plt.show()
这给出了下图:

几何查询问题
此类别中的基本问题如下:
- 点位置
- 最*的邻居
- 范围搜寻
点位置
点位置的问题是计算几何学中的基本问题,给定将空间划分为不相交的区域,我们需要查询包含目标位置的区域。
最基本的点位置问题是由单个几何对象(例如,圆形或多边形)给定分区的问题。 对于通过模块sympy.geometry中的任何类构造的那些简单对象,我们有两种有用的方法:.encloses_point和.encloses。
前者检查点是否在源对象的内部(但不在边界上),而后者检查另一个目标对象的所有定义实体是否都在源对象的内部:
In [1]: from sympy.geometry import Point, Circle, Triangle
In [2]: P1 = Point(0, 0); \
...: P2 = Point(1, 0); \
...: P3 = Point(-1, 0); \
...: P4 = Point(0, 1)
In [3]: C = Circle(P2, P3, P4); \
...: T = Triangle(P1, P2, P3)
In [4]: print "Is P1 inside of C?", C.encloses_point(P1)
Is P1 inside of C? True
In [5]: print "Is T inside of C?", C.encloses(T)
Is T inside of C? False
特别简单的是这种简单的设置,其中源对象是多边形。 sympy.geometry模块中的例程可以完成工作,但是要付出太多资源和太多时间的代价。 解决此问题的更快方法是使用matplotlib.path库中的Path类。 让我们看看如何进行快速会话。 首先,我们将多边形表示为Path:
提示
有关类Path及其在matplotlib库中的用法的信息,请参考中的官方文档 http://matplotlib.org/api/path_api.html#matplotlib.path.Path , 以及 http://matplotlib.org/users/path_tutorial.html 上的教程。
In [6]: import numpy as np, matplotlib.pyplot as plt; \
...: from matplotlib.path import Path; \
...: from chapter6 import read_poly; \
...: from scipy.spatial import ConvexHull
In [7]: superior = read_poly("superior.poly")
In [8]: hull = ConvexHull(superior['vertices'])
In [9]: my_polygon = Path([hull.points[x] for x in hull.vertices])
现在,我们可以问一个点(分别是点序列)是否在多边形内部。 我们可以通过contains_point或contains_points完成此操作:
In [10]: X = .25 * np.random.randn(100) + .5; \
....: Y = .25 * np.random.randn(100) - .5
In [11]: my_polygon.contains_points([[X[k], Y[k]] for k in range(len(X))])
Out[11]:
array([False, False, True, False, True, False, False, False, True,
False, False, False, True, False, True, False, False, False,
True, False, True, True, False, False, False, False, False,
...
True, False, True, False, False, False, False, False, True,
True, False, True, True, True, False, False, False, False,
False], dtype=bool)
当我们的空间被复杂的结构分隔时,会出现更具挑战性的点定位问题。 例如,一旦计算了三角剖分并考虑了随机位置,我们就需要查询目标位置所在的三角形。 在模块scipy.spatial中,我们有方便的例程可以通过scipy.spatial.Delaunay创建的 Delaunay 三角剖分执行此任务。 在以下示例中,我们跟踪在域中包含一组 100 个随机点的三角形:
In [12]: from scipy.spatial import Delaunay, tsearch
In [13]: tri = Delaunay(superior['vertices'])
In [14]: points = zip(X, Y)
In [15]: print tsearch(tri, points)
[ -1 687 -1 647 -1 -1 -1 -1 805 520 460 647 580 -1 -1 -1 -1 304 -1 -1 -1 -1 108 723 -1 -1 -1 -1 -1 -1 -1 144 454 -1 -1 -1 174 257 -1 -1 -1 -1 -1 52 -1 -1 985 -1 263 -1 647 -1 314 -1 -1 104 144 -1 -1 -1 -1 348 -1 368 -1 -1 -1 988 -1 -1 -1 348 614 -1 -1 -1 -1 -1 -1 -1 114 -1 -1 684 -1 537 174 161 647 702 594 687 104 -1 144 -1 -1 -1 684 -1]
提示
使用 Delaunay 对象tri的方法.find_simplex获得相同的结果:
In [16]: print tri.find_simplex(points)
[ -1 687 -1 647 -1 -1 -1 -1 805 520 460 647 580 -1 -1 -1 -1 304 -1 -1 -1 -1 108 723 -1 -1 -1 -1 -1 -1 -1 144 454 -1 -1 -1 174 257 -1 -1 -1 -1 -1 52 -1 -1 985 -1 263 -1 647 -1 314 -1 -1 104 144 -1 -1 -1 -1 348 -1 368 -1 -1 -1 988 -1 -1 -1 348 614 -1 -1 -1 -1 -1 -1 -1 114 -1 -1 684 -1 537 174 161 647 702 594 687 104 -1 144 -1 -1 -1 684 -1]
注意,当找到三角形时,例程在tri.simplices中报告其相应的索引。 如果未找到三角形(这意味着该点在三角测量的凸包外部),则报告的索引为-1。
最*的邻居
查找包含给定位置的 Voronoi 细胞的问题等同于在一组种子中搜索最*的邻居。 我们始终可以使用蛮力算法执行此搜索-在某些情况下可以接受该搜索-但总的来说,对于此问题,有更优雅,更简单的方法。 关键在于 k-d 树的概念—一种特殊的二进制空间划分结构,用于组织点,有助于快速搜索。
在 SciPy 堆栈中,我们有一个 k-d 树的实现。 模块scipy.spatial中的 Python 类KDTree。 此实现基于 Maneewongvatana 和 Mount 在 1999 年发布的想法。 使用输入点的位置进行初始化。 创建后,可以使用以下方法和属性对其进行操作和查询:
- 方法如下:
data:显示输入。leafsize:这是算法切换到蛮力的点数。 可以在KDTree对象的初始化中提供此值。m:这是点所在空间的尺寸。n:这是输入点数。maxes:这表示输入点的每个坐标的最大值。mins:这表示输入点的每个坐标的最小值。
- 属性如下:
query(self, Q, p=2.0):这是使用 k-d 树的结构相对于 Minkowski p 距离搜索最*的邻居或目标位置Q的属性。query_ball_point(self, Q, r, p=2.0):这是一种更为复杂的查询,它从目标位置Q输出 Minkowski p 距离r内的所有点。query_pairs(self, r, p=2.0):查找所有 Minkowski p 距离最大为r的点对。query_ball_tree(self, other, r, p=2.0):类似于query_pairs,但是它从两个不同的 k-d 树中找到所有成对的点,这些树的 Minkowski p 距离至少为r。sparse_distance_matrix(self, other, max_distance):这将计算两个 kd 树之间的距离矩阵,并且任何大于max_distance的距离都为零。 输出存储在稀疏dok_matrix中。count_neighbors(self, other, r, p=2.0):此属性是 Gray 和 Moore 设计的两点关联的实现,用于计算来自两个不同 k-d 树的点对的数量,这些树的 Minkowski p 距离不大于r。 与query_ball不同,此属性不会产生实际的对。
在 Cython 中,将这种对象作为扩展类型创建为cdef类cKDTree,可以实现更快的实现。 主要区别在于每种情况下节点的编码方式:
- 对于
KDTree,节点是嵌套的 Python 类(该节点是顶级类,而 leafnode 和 innernode 都是代表树中不同种类节点的子类)。 - 对于
cKDTree,节点是 C 类型的 malloc 结构,而不是类。 这使得实现快得多,但代价是对节点的可能操纵的控制较少。
让我们用这个想法来解决点定位问题,同时从苏必利尔湖重新审视 Voronoi 图:
In [17]: from scipy.spatial import cKDTree, Voronoi, voronoi_plot_2d
In [18]: vor = Voronoi(superior['vertices']); \
....: tree = cKDTree(superior['vertices'])
首先,我们查询前一个数据集,该数据集包含 100 个随机位置,即每个位置最*的种子:
In [19]: tree.query(points)
Out[19]:
(array([ 0.38942726, 0.05020313, 0.06987993, 0.2150344 , 0.16101652, 0.08485664, 0.33217896, 0.07993277, 0.06298875, 0.07428273, 0.1817608 , 0.04084714,
0.0094284 , 0.03073465, 0.01236209, 0.02395969, 0.17561544, 0.16823951, 0.24555293, 0.01742335, 0.03765772, 0.20490015, 0.00496507]),
array([ 3, 343, 311, 155, 370, 372, 144, 280, 197, 144, 251, 453, 42 233, 232, 371, 280, 311, 0, 307, 507, 49, 474, 370, 114, 5, 1, 372, 285, 150, 361, 84, 43, 98, 418, 482, 155, 144, 371, 113, 91, 3, 453, 91, 311, 412, 155, 156, 251, 251, 22, 179, 394, 189, 49, 405, 453, 506, 407, 36, 308, 33, 81, 46, 301, 144, 280, 409, 197, 407, 516]))
请注意,输出是具有两个numpy.array的元组:第一个指示每个点到其最*种子(它们的最*邻居)的距离,第二个指示相应种子的索引。
我们可以使用这种想法来表示 Voronoi 图,而无需根据顶点,线段和射线进行几何描述:
In [20]: X = np.linspace( 0.45, 0.50, 256); \
....: Y = np.linspace(-0.40, -0.35, 256); \
....: canvas = np.meshgrid(X, Y); \
....: points = np.c_[canvas[0].ravel(), canvas[1].ravel()]; \
....: queries = tree.query(points)[1].reshape(256, 256)
In [21]: ax1 = plt.subplot(121, aspect='equal'); \
....: voronoi_plot_2d(vor, ax=ax1); \
....: plt.xlim( 0.45, 0.50); \
....: plt.ylim(-0.40, -0.35)
Out[21]: (-0.4, -0.35)
In [22]: ax2 = plt.subplot(122, aspect='equal'); \
....: plt.gray(); \
....: plt.pcolor(X, Y, queries); \
....: plt.plot(vor.points[:,0], vor.points[:,1], 'ro'); \
....: plt.xlim( 0.45, 0.50); \
....: plt.ylim(-0.40, -0.35); \
....: plt.show()
这在苏必利尔湖北岸的一个小窗口中提供了 Voronoi 图的以下两种不同表示形式:

范围搜寻
范围搜索问题试图确定输入集中的哪些对象与查询对象(我们称为范围)相交。 例如,当给定*面中的一组点时,哪些点包含在以目标位置Q为中心的半径r的圆内? 我们可以通过 k-d 树的适当实现轻松地通过属性query_ball_point解决此样本问题。 如果范围是由一系列不同球的并集形成的对象,我们可以走得更远。 相同的属性可以完成工作,如以下代码所示:
In [23]: points = np.random.rand(320, 2); \
....: range_points = np.random.rand(5, 2); \
....: range_radii = 0.1 * np.random.rand(5)
In [24]: tree = cKDTree(points); \
....: result = set()
In [25]: for k in range(5):
....: point = range_points[k]
....: radius = range_radii[k]
....: partial_query = tree.query_ball_point(point, radius)
....: result = result.union(set(partial_query))
....:
In [26]: print result
set([130, 3, 166, 231, 40, 266, 2, 269, 120, 53, 24, 281, 26, 284])
In [27]: fig = plt.figure(); \
....: plt.axes().set_aspect('equal')
In [28]: for point in points:
....: plt.plot(point[0], point[1], 'ko')
....:
In [29]: for k in range(5):
....: point = range_points[k]
....: radius = range_radii[k]
....: circle = plt.Circle(point, radius, fill=False)
....: fig.gca().add_artist(circle)
....:
In [30]: plt.show()
这给出了下图,其中小点代表搜索空间的位置,圆圈是范围。 查询是位于圆内的一组点,由我们的算法计算得出:

根据输入对象类型,范围类型和查询类型,此设置中的问题从微不足道到极其复杂。 关于这一主题的一个很好的论述是美国数学协会出版社于 1999 年由 Pankaj K. Agarwal 和 Jeff Erickson 出版的调查论文几何范围搜索及其相关术语,作为 Advances 的一部分 离散和计算几何:1996 年 AMS-IMS-SIAM 联合夏季研究会议论文集,离散和计算几何。
动态问题
动态问题被视为前两个设置(静态或查询)中的任何问题,但又增加了挑战,即不断插入或删除对象。 除了解决基本问题外,我们还需要采取其他措施,以确保针对这些更改实施有效。
为此,模块scipy.spatial中的Qhull库包装的实现可以处理新点的插入。 我们通过说明选项incremental=True来完成此操作,该选项基本上抑制了qhull控件'Qz'并为这些复杂情况准备了输出结构。
让我们用一个简单的例子来说明这一点。 我们从苏必利尔湖的前十个顶点开始,然后一次插入十个顶点,并更新相应的三角剖分和 Voronoi 图:
In [27]: from scipy.spatial import delaunay_plot_2d
In [28]: small_superior = superior['vertices'][:9]
In [29]: tri = Delaunay(small_superior, incremental=True); \
....: vor = Voronoi(small_superior, incremental=True)
In [30]: for k in range(4):
....: tri.add_points(superior['vertices'][10*(k+1):10*(k+2)-1])
....: vor.add_points(superior['vertices'][10*(k+1):10*(k+2)-1])
....: ax1 = plt.subplot(4, 2, 2*k+1, aspect='equal')
....: delaunay_plot_2d(tri, ax1)
....: ax1.set_xlim( 0.00, 1.00)
....: ax1.set_ylim(-0.70, -0.30)
....: ax2 = plt.subplot(4, 2, 2*k+2, aspect='equal')
....: voronoi_plot_2d(vor, ax2)
....: ax2.set_xlim(0.0, 1.0)
....: ax2.set_ylim(-0.70, -0.30)
....:
In [4]: plt.show()
这将显示以下图:

数值计算几何
该领域同时出现在寻求先验无关问题解决方案的不同研究人员群体中。 事实证明,它们提出的所有解实际上都具有重要的公分母,它们是通过参数曲线,参数曲面或由其限定的区域表示对象时获得的。 这些年来,这些科学家最终统一了他们的技术,最终定义了数值计算几何学的领域。 在此过程中,该领域获得了不同的名称:机器几何,几何建模以及最广泛的计算机辅助几何设计( CAGD )。
它用于计算机视觉,例如用于 3D 重建和运动轮廓。 它被广泛用于汽车,飞机或船只的车身的设计和定性分析。 有很多计算机辅助设计( CAD )软件包,这些软件包有助于进行交互式操作并解决该领域的许多问题。 在这方面,与 Python 的任何交互都被降级为可视化或动画背后的基础计算引擎的一部分—这不是 SciPy 的优势。 因此,我们将不在本书中介绍可视化或动画应用程序,而将重点放在基础数学上。
在这方面,数值计算几何的基础是基于三个关键概念的:贝塞尔曲面,库恩色块和 B 样条方法。 反过来,贝塞尔曲线理论在这些概念的发展中起着核心作用。 它们是分段多项式曲线表示的几何标准。 在本节中,我们仅关注*面贝塞尔曲线理论的基本发展。
提示
其余材料也超出了 SciPy 的范围,因此我们将其说明留给更多的技术书籍。 从这个意义上讲,最好的来源无疑是 Gerald Farin 所著的《计算机辅助几何设计的曲线和曲面-实用指南》(第 5 版),该书由 Morgan Kauffman 出版社由 Academic Press 出版。 计算机图形学和几何建模中的系列。
贝塞尔曲线
一切都从 de Casteljau 算法开始,以构造阶次为 3 的多项式的圆弧的参数方程。在子模块matplotlib.path中,我们使用类Path实现了该算法的实现,可用于生成我们的算法。 自己的用户定义例程,用于生成和绘制*面贝塞尔曲线:
# file chapter6.py ...continued
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from matplotlib.path import Path
def bezier_parabola(P1, P2, P3):
return Path([P1, P2, P3],
[Path.MOVETO, Path.CURVE3, Path.CURVE3])
def bezier_cubic(P1, P2, P3, P4):
return Path([P1, P2, P3, P4],
[Path.MOVETO, Path.CURVE4, Path.CURVE4, Path.CURVE4])
def plot_path(path, labels=None):
Xs, Ys = zip(*path.vertices)
fig = plt.figure()
ax = fig.add_subplot(111, aspect='equal')
ax.set_xlim(min(Xs)-0.2, max(Xs)+0.2)
ax.set_ylim(min(Ys)-0.2, max(Ys)+0.2)
patch = patches.PathPatch(path, facecolor='none', linewidth=2)
ax.add_patch(patch)
ax.plot(Xs, Ys, 'o--', color='blue', linewidth=1)
if labels:
for k in range(len(labels)):
ax.text(path.vertices[k][0]-0.1,
path.vertices[k][1]-0.1,
labels[k])
plt.show()
在继续之前,我们需要对先前的代码进行一些解释:
- 通过创建以三个控制点为顶点,列表
[Path.MOVETO, Path.CURVE3, Path.CURVE3]为代码的Path来执行阶数为 2 的多项式弧的 de Casteljau 算法。 这确保了所得曲线沿段P1P2给出的方向在P1处开始,并沿段P2P3给出的方向在P3处结束。 如果这三个点是共线的,我们将得到一个包含所有点的线段。 否则,我们将获得抛物线弧。 - 用于阶数为 3 的多项式弧的 de Casteljau 算法的执行方式与之前的情况类似。 我们有四个控制点,并使用它们作为顶点创建一个
Path。 代码是列表[Path.MOVETO, Path.CURVE4, Path.CURVE4, Path.CURVE4],该列表确保弧线以段P1P2给出的方向从P1开始。 它还确保弧在段P3P4的方向上在P4处终止。
让我们用一些基本示例进行测试:
In [1]: import numpy as np; \
...: from chapter6 import *
In [2]: P1 = (0.0, 0.0); \
...: P2 = (1.0, 1.0); \
...: P3 = (2.0, 0.0); \
...: path_1 = bezier_parabola(P1, P2, P3); \
...: plot_path(path_1, labels=['P1', 'P2', 'P3'])
这给出了所需的抛物线弧:

In [3]: P4 = (2.0, -1.0); \
...: P5 = (3.0, 0.0); \
...: path_2 = bezier_cubic(P1, P2, P4, P5); \
...: plot_path(path_2, labels=['P1', 'P2', 'P4', 'P5'])
如下图所示,这提供了很好的三次方弧:

更高阶的曲线在计算上评估起来很昂贵。 当需要复杂路径时,我们宁愿将它们创建为修补在一起的低阶 Bézier 曲线的分段序列,我们将此对象称为 Bézier 样条。 注意,不难保证这些样条线的连续性。 使每个路径的末尾成为下一条路径的起点就足够了。 通过将一条曲线的最后两个控制点与下一条曲线的前两个控制点对齐,也很容易保证*滑度(至少达到一阶导数)。 让我们用一个例子来说明这一点:
In [4]: Q1 = P5; \
...: Q2 = (4.0, 0.0); \
...: Q3 = (5.0, -1.0); \
...: Q4 = (6.0, 0.0); \
...: path_3 = bezier_cubic(P1, P2, P3, P5); \
...: path_4 = bezier_cubic(Q1, Q2, Q3, Q4); \
...: plot_path(Path.make_compound_path(path_3, path_4),
labels=['P1','P2','P3','P5=Q1',
'P5=Q1','Q2','Q3', 'Q4'])
这给出了以下贝塞尔样条:

当我们需要对曲线应用仿射变换时,将曲线表示为贝塞尔曲线的明显优势就显现了。 例如,如果我们需要最后一条曲线的逆时针旋转版本,而不是对曲线的所有点执行操作,我们只需将变换应用于控制点并在新控件上重复 de Casteljau 算法即可:
In [5]: def rotation(point, angle):
...: return (np.cos(angle)*point[0] - np.sin(angle)*point[1],
...: np.sin(angle)*point[0] + np.cos(angle)*point[1])
...:
In [6]: new_Ps = [rotation(P, np.pi/3) for P in path_3.vertices]; \
...: new_Qs = [rotation(Q, np.pi/3) for Q in path_4.vertices]; \
...: path_5 = bezier_cubic(*new_Ps); \
...: path_6 = bezier_cubic(*new_Qs); \
...: plot_path(Path.make_compound_path(path_5, path_6))
这将显示以下结果:

摘要
在本章中,我们简要介绍了计算几何学领域,并且掌握了 SciPy 堆栈中编码的所有工具,以有效解决该主题中最常见的问题。
在接下来的两章中,我们将探索 SciPy 在统计,数据挖掘,学习理论和其他技术应用于定量数据分析领域的能力。
七、描述性统计
本章及下一章主要针对 SAS,SPSS 或 Minitab 用户,尤其是那些使用 R 或 S 语言进行统计计算的用户。 我们将在 IPython 会话的帮助下开发一个环境,以在数据分析领域有效工作,该会话由 SciPy 堆栈中的以下资源提供支持:
- 符号计算库
sympy.stats的概率和统计子模块。 - 统计函数
scipy.stats和scipy.stats.mstats(后者用于由掩码数组提供的数据)的两个库,以及模块statsmodels,用于数据探索,统计模型的估计以及在数值设置中执行统计测试。 软件包statsmodels在后台使用了功能强大的库patsy来描述统计模型和使用 Python 构建建筑矩阵(R 或 S 用户会发现patsy与他们的公式微型语言兼容)。 - 对于统计推断,我们再次使用
scipy.stats和statsmodels(用于频度推断和似然推断)以及实现了贝叶斯统计模型和拟合算法(包括马尔可夫链蒙特卡洛)的模块pymc。 - 两个高级数据处理工具的强大功能库。
- Wes McKinney 创建的 Python 数据分析库
pandas以类似于 SQL 的方式处理时间序列,数据对齐和数据库处理的有用功能。 - 包
PyTables由 Francesc Alted,Ivan Vilata 等创建,用于管理分层数据集。 它旨在高效,轻松地处理大量数据。
- Wes McKinney 创建的 Python 数据分析库
- 用于矢量量化的聚类模块
scipy.cluster,k-means 算法,分层聚类聚类。 - 一些 SciPy 工具箱(简称 SciKit):
scikit-learn:一组用于机器学习和数据挖掘的 Python 模块。scikits.bootstrap:引导置信区间估计例程。
要掌握这些章节中的技术,需要具备明显的统计知识。 任何优秀的基础教科书,只要有大量示例和问题就可以了。 为了对推断进行更深入的研究,我们建议由 George Casella 和 Roger L. Berger 于 2002 年由 Duxbury 出版的第二版统计推断。
以下python库的文档可以通过其相应的官方页面在线获得:
sympy.stats: http://docs.sympy.org/dev/modules/stats.html 。scipy.stats和scipy.stats.mstats: http://docs.scipy.org/doc/scipy/reference/stats.html 获取功能列表, http://docs.scipy。 org / doc / scipy / reference / tutorial / stats.html 提供了不错的概述和教程。scipy.cluster: http://docs.scipy.org/doc/scipy/reference/cluster.htmlPyTables: http://www.pytables.org/PyMC: http://pymc-devs.github.io/pymc/index.html
最好地了解 Pandas 的方法无疑是 Wes McKinney 本人(该库的创建者)所著的 Python for Data Analysis:与 Pandas,NumPy 和 IPython 进行数据争辩。 必须熟悉 SQL,为此,我们的建议是在线获得良好的培训。
了解模型估计主题的最佳资源之一是 Joseph Hilbe 和 Andrew Robinson 所著的统计模型估计方法。 尽管此资源中的所有代码都是为 R 编写的,但它们很容易移植到scipy.stats,statsmodels,PyMC和scikit-learn中的例程和类的组合。
软件包statsmodels曾经是scikit-learn工具包的一部分。 优秀的文档可用来学习此软件包的用法和功能,以及用于描述统计模型(patsy)的底层库,始终是其创建者在线提供的教程:
对于scipy工具包,可以通过其页面 http://scikits.appspot.com/ 找到最佳资源。 浏览不同的工具箱将为我们提供良好的教程和更多参考。 特别是对于scikit-learn,必读的两个文章是的官方页面:http://scikit-learn.org/stable/ 和开创性的文章 Scikit-learn:机器学习 Fabian Pedregosa 等人的 Python ,于 2011 年发表在机器学习研究杂志上。
但是由于这是本书的习俗,因此我们将从材料本身的角度来开发材料。 因此,我们将论述分为两章,第一章涉及概率论和统计学中最基本的主题:
- 概率-随机变量及其分布。
- 数据浏览。
在下一章中,我们将讨论“统计和数据分析”中的更多高级主题:
- 统计推断。
- 机器学习。 可以从数据中学习的系统的构建和研究。 机器学习的重点是基于从某些训练数据中学到的已知属性进行的预测。
- 数据挖掘。 在大型数据集中发现模式。 数据挖掘专注于发现数据中的先验未知属性。
动机
1857 年 9 月 8 日,星期二,中美洲 SS 汽船于上午 9 点离开哈瓦那。 纽约,载有约 600 名乘客和机组人员。 在这艘船内,存放了珍贵的货物-约翰·詹姆斯·奥杜邦(John James Audubon)的一组手稿以及三吨金条和金币。 这些手稿记录了一次穿越尚未成名的美国西南部和加利福尼亚州的探险之旅,其中包含 200 张有关其野生动植物的素描和绘画。 黄金是加利福尼亚淘金热期间多年探矿和开采的成果,旨在重振船上许多乘客的生活。
9 日,船只撞上了暴风雨,随后演变成飓风。 轮船在海上经历了四天的艰辛,到了星期六早上,这艘船已经注定了。 船长安排将妇女和儿童带到海军陆战队,后者在中午左右为他们提供帮助。 尽管余下的船员和乘客竭尽全力挽救了这艘船,但这不可避免的发生在大约晚上 8 点。 同一天。 沉船夺去了 425 名士兵的生命,并将这些宝贵的货物运到海底。
直到 1980 年代后期,技术才使深海沉船得以恢复。 但是,没有站点的准确位置,任何技术都将无济于事。 在下面的段落中,我们将通过执行简单的仿真来说明 SciPy 堆栈的功能。 目的是为中美洲 SS 残骸的可能位置创建数据集。 我们挖掘这些数据以尝试找出最可能的目标。
我们模拟了上午 7 点之间汽船的几种可能路径(例如 10,000 种随机生成的可能性)。 周六和 13 小时后,周日晚上 8 点。 上午 7 点 在那个星期六,船长威廉·赫恩登(William Herndon)进行了天体修复,并口头将其位置转交给纵帆船 El Dorado。 固定为 31º25'N,77º10'W。 由于该船当时无法操作-没有引擎,也没有帆-在接下来的 13 个小时内,其航向完全受到洋流和风的影响。 有了足够的信息,就可以在不同的可能路径上对漂移和回旋进行建模。
我们首先创建一个DataFrame-一种计算结构,它将以非常有效的方式保存我们需要的所有值。 我们在pandas库的帮助下做到了:
In [1]: from datetime import datetime, timedelta; \
...: from dateutil.parser import parse
In [2]: interval = [parse("9/12/1857 7 am")]
In [3]: for k in range(14*2-1):
...: if k % 2 == 0:
...: interval.append(interval[-1])
...: else:
...: interval.append(interval[-1] + timedelta(hours=1))
...:
In [4]: import numpy as np, pandas as pd
In [5]: herndon = pd.DataFrame(np.zeros((28, 10000)),
...: index = [interval, ['Lat', 'Lon']*14])
DataFrame herndon的每一列用于保存每小时采样一次的 SS Central America 可能路径的纬度和经度。 例如,要观察第一个路径,我们发出以下命令:
In [6]: herndon[0]
Out[6]:
1857-09-12 07:00:00 Lat 0
Lon 0
1857-09-12 08:00:00 Lat 0
Lon 0
1857-09-12 09:00:00 Lat 0
Lon 0
1857-09-12 10:00:00 Lat 0
Lon 0
1857-09-12 11:00:00 Lat 0
Lon 0
1857-09-12 12:00:00 Lat 0
Lon 0
1857-09-12 13:00:00 Lat 0
Lon 0
1857-09-12 14:00:00 Lat 0
Lon 0
1857-09-12 15:00:00 Lat 0
Lon 0
1857-09-12 16:00:00 Lat 0
Lon 0
1857-09-12 17:00:00 Lat 0
Lon 0
1857-09-12 18:00:00 Lat 0
Lon 0
1857-09-12 19:00:00 Lat 0
Lon 0
1857-09-12 20:00:00 Lat 0
Lon 0
Name: 0, dtype: float64
劳伦斯·D·斯通(Lawrence D.Stone)在 2010 年国际信息融合会议上的《中美洲 SS 搜索》一文中的劳伦斯·D·斯通(Lawrence D.Stone)所解释的分析之后,让我们根据与哥伦布美国发现组织进行的类似分析来填充此数据。
赫恩登上尉在凌晨 7 点获得的天体定位。 是在暴风雨中与六分仪一起拍摄的。 用这种方法估算的经度和在这些天气条件下存在一些不确定性,这些不确定性由均值(0,0),标准差为 0.9 海里(对于纬度)的双变量正态分布随机变量建模,并且 3.9 海里(经度)。 我们首先创建具有这些特征的随机变量。 让我们用这个想法用几个随机的初始位置填充数据框:
In [7]: from scipy.stats import multivariate_normal
In [8]: celestial_fix = multivariate_normal(cov = np.diag((0.9, 3.9)))
提示
为了估算相应的天体定位以及所有进一步的大地测量,我们将使用 Vincenty 的椭球的精确公式,假设在a = 6378137米赤道的半径和f = 1/298.257223563椭球的展*度(这些数字 被视为制图,大地测量和导航的标准之一,并被社区称为“世界大地测量系统 WGS-84 椭球”。
可以在 https://github.com/blancosilva/Mastering-Scipy/blob/master/chapter7/Geodetic_py.py 中找到一组用 Python 编码的非常好的公式。 有关这些公式背后的描述和理论,请阅读 Wikipedia 上的出色调查,网址为 https://en.wikipedia.org/wiki/Vincenty%27s_formulae 。
特别地,在此示例中,我们将使用 Vincenty 的直接公式,该公式计算从纬度phi1,经度L1开始的对象的最终纬度phi2,经度L2和方位角s2,以及 以初始方位角s1行驶s米。 纬度,经度和方位角以度为单位,距离以米为单位。 我们还使用为负值分配西纬度的惯例。 要将海里或节的转换应用于国际单位制中的相应单位,我们采用了scipy.constants中的单位制。
In [9]: from Geodetic_py import vinc_pt; \
...: from scipy.constants import nautical_mile
In [10]: a = 6378137.0; \
....: f = 1./298.257223563
In [11]: for k in range(10000):
....: lat_delta,lon_delta = celestial_fix.rvs() * nautical_mile
....: azimuth = 90 - np.angle(lat_delta+1j*lon_delta, deg=True)
....: distance = np.hypot(lat_delta, lon_delta)
....: output = vinc_pt(f, a, 31+25./60,
....: -77-10./60, azimuth, distance)
....: herndon.ix['1857-09-12 07:00:00',:][k] = output[0:2]
....:
In [12]: herndon.ix['1857-09-12 07:00:00',:]
Out[12]:
0 1 2 3 4 5
Lat 31.455345 31.452572 31.439491 31.444000 31.462029 31.406287
Lon -77.148860 -77.168941 -77.173416 -77.163484 -77.169911 -77.168462
6 7 8 9 ... 9990
Lat 31.390807 31.420929 31.441248 31.367623 ... 31.405862
Lon -77.178367 -77.187680 -77.176924 -77.172941 ... -77.146794
9991 9992 9993 9994 9995 9996
Lat 31.394365 31.428827 31.415392 31.443225 31.350158 31.392087
Lon -77.179720 -77.182885 -77.159965 -77.186102 -77.183292 -77.168586
9997 9998 9999
Lat 31.443154 31.438852 31.401723
Lon -77.169504 -77.151137 -77.134298
[2 rows x 10000 columns]
我们根据公式D = (V + leeway * W)模拟漂移。 在此公式中,V(洋流)被建模为指向东北(约 45 度方位角)和 1 至 1.5 节之间的可变速度的向量。 另一个随机变量W代表飓风期间该地区风的作用,我们选择用南和东之间的方向表示,*均速度为 0.2 节,标准偏差为 1/30 结。 两个随机变量均编码为双变量正态。 最后,我们考虑了余地因素。 根据对 SS 中美洲蓝图进行的一项研究,我们估计这一余地约为 3%:
提示
代表海流和风的随机变量的选择不同于上述论文中使用的变量。 在我们的版本中,我们没有使用 Stone 从海军海洋数据中心接收的数据计算出的实际协方差矩阵。 相反,我们提供了一个非常简化的版本。
In [13]: current = multivariate_normal((np.pi/4, 1.25),
....: cov=np.diag((np.pi/270, .25/3))); \
....: wind = multivariate_normal((np.pi/4, .3),
....: cov=np.diag((np.pi/12, 1./30))); \
....: leeway = 3./100
In [14]: for date in pd.date_range('1857-9-12 08:00:00',
....: periods=13, freq='1h'):
....: before = herndon.ix[date-timedelta(hours=1)]
....: for k in range(10000):
....: angle, speed = current.rvs()
....: current_v = speed * nautical_mile * (np.cos(angle)
....: + 1j * np.sin(angle))
....: angle, speed = wind.rvs()
....: wind_v = speed * nautical_mile * (np.cos(angle)
....: + 1j * np.sin(angle))
....: drift = current_v + leeway * wind_v
....: azimuth = 90 - np.angle(drift, deg=True)
....: distance = abs(drift)
....: output = vinc_pt(f, a, before.ix['Lat'][k],
....: before.ix['Lon'][k],
....: azimuth, distance)
....: herndon.ix[date,:][k] = output[:2]
让我们绘制这些模拟路径的前三个:
In [15]: import matplotlib.pyplot as plt; \
....: from mpl_toolkits.basemap import Basemap
In [16]: m = Basemap(llcrnrlon=-77.4, llcrnrlat=31.2,urcrnrlon=-76.6,
....: urcrnrlat=31.8, projection='lcc', lat_0 = 31.5,
....: lon_0=-77, resolution='l', area_thresh=1000.)
In [17]: m.drawmeridians(np.arange(-77.4,-76.6,0.1),
....: labels=[0,0,1,1]); \
....: m.drawparallels(np.arange(31.2,32.8,0.1),labels=[1,1,0,0]);\
....: m.drawmapboundary()
In [18]: colors = ['r', 'b', 'k']; \
....: styles = ['-', '--', ':']
In [19]: for k in range(3):
....: latitudes = herndon[k][:,'Lat'].values
....: longitudes = herndon[k][:,'Lon'].values
....: longitudes, latitudes = m(longitudes, latitudes)
....: m.plot(longitudes, latitudes, color=colors[k],
....: lw=3, ls=styles[k])
....:
In [20]: plt.show()
这向我们展示了 SS 中美洲在风暴中漂移时遵循的这三种可能的路径。 正如预期的那样,他们观察到的是东北大方向,有时会显示与强风影响的偏离:

但是,此仿真的重点是所有这些路径的最终位置。 首先,将它们全部绘制在同一张地图上,以进行快速的视觉评估:
In [21]: latitudes, longitudes = herndon.ix['1857-9-12 20:00:00'].values
In [22]: m = Basemap(llcrnrlon=-82., llcrnrlat=31, urcrnrlon=-76,
....: urcrnrlat=32.5, projection='lcc', lat_0 = 31.5,
....: lon_0=-78, resolution='h', area_thresh=1000.)
In [23]: X, Y = m(longitudes, latitudes)
In [24]: x, y = m(-81.2003759, 32.0405369) # Savannah, GA
In [25]: m.plot(X, Y, 'ko', markersize=1); \
....: m.plot(x,y,'bo'); \
....: plt.text(x-10000, y+10000, 'Savannah, GA'); \
....: m.drawmeridians(np.arange(-82,-76,1), labels=[1,1,1,1]); \
....: m.drawparallels(np.arange(31,32.5,0.25), labels=[1,1,0,0]);\
....: m.drawcoastlines(); \
....: m.drawcountries(); \
....: m.fillcontinents(color='coral'); \
....: m.drawmapboundary(); \
....: plt.show()

为了更好地估计沉船的真实位置,可以通过使用挪威树皮埃伦的约翰逊船长的信息来扩展模拟。 这艘船在上午 8 点救出了几名幸存者。 在星期天,在 31º55'N,76º13'W 的记录位置。 我们可以采用类似的技术使用反向漂移来追溯到船只沉没的位置。 对于这种情况,天体中的不确定性可以通过标准偏差为 0.9(纬度)和 5.4 海里(经度)的双变量正态分布来建模。
提示
使用 El Dorado 的信息也可以进行第三次仿真,但是我们在计算中没有考虑到这一点。
由于此时唯一相关的信息是沉船的位置,因此我们不需要将中间位置保留在模拟路径中。 我们将数据记录在pandas Series中:
In [26]: interval = []
In [27]: for k in range(10000):
....: interval.append(k)
....: interval.append(k)
....:
In [28]: ellen = pd.Series(index = [interval, ['Lat','Lon']*10000]);\
....: celestial_fix =multivariate_normal(cov=np.diag((0.9,5.4)));\
....: current = multivariate_normal((225, 1.25),
....: cov=np.diag((2./3, .25/3)))
In [29]: for k in range(10000):
....: lat_delta, lon_delta = celestial_fix.rvs()*nautical_mile
....: azimuth = 90 - np.angle(lat_delta+1j*lon_delta, deg=True)
....: distance = np.hypot(lat_delta, lon_delta)
....: output = vinc_pt(f, a, 31+55./60,
....: -76-13./60, azimuth, distance)
....: ellen[k] = output[0:2]
....:
In [30]: for date in pd.date_range('1857-9-13 07:00:00', periods=12,
....: freq='-1h'):
....: for k in range(10000):
....: angle, speed = current.rvs()
....: output = vinc_pt(f, a, ellen[k,'Lat'],
....: ellen[k,'Lon'], 90-angle, speed)
....: ellen[k]=output[0:2]
....:
模拟的目的是构建地图,该地图指示根据纬度和经度发现沉船的可能性。 我们可以通过对模拟数据执行内核密度估计来构造它。 这种情况下的困难在于使用正确的度量。 不幸的是,我们无法在 SciPy 中基于 Vincenty 的公式创建适用于此操作的度量,但是,我们有两个选择:
- 使用库
scipy.stats中的例程gaussian_kde,在小区域内进行线性*似 - 使用工具包
scikit-learn中的类KernelDensity进行球面*似,并采用 Harvesine 度量和球树算法
第一种方法的优点是速度更快,并且最佳带宽的计算是在内部完成的。 如果我们能够提供正确的带宽,则第二种方法更准确。 无论如何,我们都将模拟用作训练数据以相同的方式准备数据:
In [31]: training_latitudes, training_longitudes = herndon.ix['1857-9-12 20:00:00'].values; \
....: training_latitudes = np.concatenate((training_latitudes,
....: ellen[:,'Lat'])); \
....: training_longitudes = np.concatenate((training_longitudes,
....: ellen[:,'Lon'])); \
....: values = np.vstack([training_latitudes,
....: training_longitudes]) * np.pi/180.
对于线性*似,我们执行以下计算:
In [32]: from scipy.stats import gaussian_kde
In [33]: kernel_scipy = gaussian_kde(values)
对于球面*似,并假设小于最佳带宽 10 -7 ,我们改为发出以下内容:
In [32]: from sklearn.neighbors import KernelDensity
In [33]: kernel_sklearn = KernelDensity(metric='haversine',
....: bandwidth=1.e-7,
....: kernel='gaussian',
....: algorithm='ball_tree')
....: kernel_sklearn.fit(values.T)
Out[33]:
KernelDensity(algorithm='ball_tree', atol=0, bandwidth=1e-07,
breadth_first=True, kernel='gaussian', leaf_size=40,
metric='haversine', metric_params=None, rtol=0)
从这里开始,我们需要做的就是生成一个映射,在其上构建一个网格,并使用这些值,对计算出的内核进行相应的评估。 这将为我们提供相应分布的概率密度函数( PDF ):
In [34]: plt.figure(); \
....: m = Basemap(llcrnrlon=-77.1, llcrnrlat=31.4,urcrnrlon=-75.9,
....: urcrnrlat=32.6, projection='lcc', lat_0 = 32,
....: lon_0=-76.5, resolution='l', area_thresh=1000);\
....: m.drawmeridians(np.arange(-77.5,-75.5,0.2),
....: labels=[0,0,1,1]); \
....: m.drawparallels(np.arange(31,33,0.2), labels=[1,1,0,0]); \
....: grid_lon, grid_lat = m.makegrid(25, 25); \
....: xy = np.vstack([grid_lat.ravel(),
....: grid_lon.ravel()]) * np.pi/180.
PDF 的计算取决于所实现的内核,如下所示:
In [35]: data = kernel_scipy(xy)
In [35]: data = np.exp(kernel_sklearn.score_samples(xy.T))
剩下的就是绘制结果。 我们显示第一种方法的结果,而将第二种方法作为一个不错的练习:
In [36]: levels = np.linspace(data.min(), data.max(), 6); \
....: data = data.reshape(grid_lon.shape)
In [37]: grid_lon, grid_lat = m(grid_lon, grid_lat); \
....: cs = m.contourf(grid_lon, grid_lat, data,
....: clevels=levels, cmap=plt.cm.Greys); \
....: cbar = m.colorbar(cs, location='bottom', pad="10%"); \
....: plt.show()
这为我们提供了大约 50 x 50(海里)的区域,并以相应的密度着色。 较暗的区域表示发现沉船的可能性更高:

注意
中美洲 SS 遗骸的实际位置是在 31º35'N,77º02'W,与我们的粗略估算结果相差不大-实际上,与传达给海军陆战队的赫恩登船长的定位非常接* 。
这个简短的激励示例演示了 SciPy 堆栈执行统计模拟,以最佳方式存储和处理结果数据以及使用最新算法分析它们以提取有价值信息的能力。 在接下来的页面中,我们将更深入地介绍这些技术。
可能性
在 SciPy 堆栈中,我们有两种方法来确定概率:符号设置和数值设置。 在此简短的部分中,我们将比较这两个示例序列。
对于随机变量的符号处理,我们使用模块sympy.stats,而对于数值处理,我们使用模块scipy.stats。 在这两种情况下,目标都是相同的-任何随机变量的实例化,以及对它们的以下三种操作:
- 描述带有数字(参数)的随机变量的概率分布。
- 对随机变量的功能描述。
- 关联概率的计算。
让我们通过两种不同设置的范围来观察几种情况。
符号设定
让我们从离散随机变量开始。 例如,让我们考虑几个随机变量,这些变量用于描述滚动三个 6 面骰子,一个 100 面骰子的过程以及可能的结果:
In [1]: from sympy import var; \
...: from sympy.stats import Die, sample_iter, P, variance, \
...: std, E, moment, cdf, density, \
...: Exponential, skewness
In [2]: D6_1, D6_2, D6_3 = Die('D6_1', 6), Die('D6_2', 6), \
...: Die('D6_3', 6); \
...: D100 = Die('D100', 100); \
...: X = D6_1 + D6_2 + D6_3 + D100
我们运行一个模拟,将这四个骰子投掷 20 次,并收集每次掷出的总和:
In [3]: for item in sample_iter(X, numsamples=20):
...: print item,
...:
45 50 84 43 44 84 102 38 90 94 35 78 67 54 20 64 62 107 59 84
让我们说明一下如何轻松计算与这些变量关联的概率。 例如,要计算出三个 6 面骰子的数量之和小于 100 面骰子的掷出次数的概率,可以按以下方式获得:
In [4]: P(D6_1 + D6_2 + D6_3 < D100)
Out[4]: 179/200
条件概率也是可以实现的,例如,“如果掷两个 6 面骰子,如果第一个骰子显示 5,则获得至少 10 的概率是多少?”:
In [5]: from sympy import Eq # Don't use == with symbolic objects!
In [6]: P(D6_1 + D6_2 > 9, Eq(D6_1, 5))
Out[6]: 1/3
相关概率分布的参数的计算也非常简单。 在下面的会话中,我们获得X的方差,标准差和期望值,以及该变量在零附*的一些其他高阶矩:
In [7]: variance(X), std(X), E(X)
Out[7]: (842, sqrt(842), 61)
In [8]: for n in range(2,10):
...: print "mu_{0} = {1}".format(n, moment(X, n, 0))
...:
mu_2 = 4563
mu_3 = 381067
mu_4 = 339378593/10
mu_5 = 6300603685/2
mu_6 = 1805931466069/6
mu_7 = 176259875749813/6
mu_8 = 29146927913035853/10
mu_9 = 586011570997109973/2
我们也可以轻松计算出概率质量函数和累积密度函数:
In [9]: cdf(X) In [10]: density(X)
Out[9]: Out[10]:
{4: 1/21600, {4: 1/21600,
5: 1/4320, 5: 1/5400,
6: 1/1440, 6: 1/2160,
7: 7/4320, 7: 1/1080,
8: 7/2160, 8: 7/4320,
9: 7/1200, 9: 7/2700,
10: 23/2400, 10: 3/800,
11: 7/480, 11: 1/200,
12: 1/48, 12: 1/160,
13: 61/2160, 13: 1/135,
14: 791/21600, 14: 181/21600,
15: 329/7200, 15: 49/5400,
16: 1193/21600, 16: 103/10800,
17: 281/4320, 17: 53/5400,
18: 3/40, 18: 43/4320,
...
102: 183/200, 102: 1/100,
103: 37/40, 103: 1/100,
104: 4039/4320, 104: 43/4320,
105: 20407/21600, 105: 53/5400,
106: 6871/7200, 106: 103/10800,
107: 20809/21600, 107: 49/5400,
108: 2099/2160, 108: 181/21600,
109: 47/48, 109: 1/135,
110: 473/480, 110: 1/160,
111: 2377/2400, 111: 1/200,
112: 1193/1200, 112: 3/800,
113: 2153/2160, 113: 7/2700,
114: 4313/4320, 114: 7/4320,
115: 1439/1440, 115: 1/1080,
116: 4319/4320, 116: 1/2160,
117: 21599/21600, 117: 1/5400,
118: 1} 118: 1/21600}
让我们进入连续随机变量。 此简短会话计算通用指数随机变量的密度和累积分布函数以及几个参数:
In [11]: var('mu', positive=True); \
....: var('t'); \
....: X = Exponential('X', mu)
In [12]: density(X)(t)
Out[12]: mu*exp(-mu*t)
In [13]: cdf(X)(t)
Out[13]: Piecewise((1 - exp(-mu*t), t >= 0), (0, True))
In [14]: variance(X), skewness(X)
Out[14]: (mu**(-2), 2)
In [15]: [moment(X, n, 0) for n in range(1,10)]
Out[15]:
[1/mu,
2/mu**2,
6/mu**3,
24/mu**4,
120/mu**5,
720/mu**6,
5040/mu**7,
40320/mu**8,
362880/mu**9]
提示
有关模块sympy.stats的完整描述以及其所有已实现的随机变量的详尽列举,请参见上的在线官方文档,网址为 http://docs.sympy.org/dev/modules/stats.html。 。
数值设定
通过实现来自模块scipy.stats的对象rv_discrete来执行数字设置中离散随机变量的描述。 该对象具有以下方法:
object.rvs获取样品object.pmf和object.logpmf分别计算概率质量函数及其对数object.cdf和object.logcdf分别计算累积密度函数及其对数object.sf和object.logsf分别计算生存函数(1-cdf)及其对数object.ppf和object.isf计算百分比点函数(CDF 的倒数)和生存函数的倒数object.expect和object.moment计算期望值和其他力矩object.entropy计算熵object.median,object.mean,object.var和object.std来计算基本参数(也可以通过object.stats方法访问)object.interval计算具有给定概率的间隔,该间隔包含分布的随机实现
然后,我们可以使用骰子模拟一个实验,类似于上一节。 在这种情况下,我们通过骰子侧面集合的均匀分布来表示骰子:
In [1]: import numpy as np, matplotlib.pyplot as plt; \
...: from scipy.stats import randint, gaussian_kde, rv_discrete
In [2]: D6 = randint(1, 7); \
...: D100 = randint(1, 101)
象征性地,构造这四个独立随机变量之和非常简单。 从数字上讲,我们以不同的方式处理这种情况。 假设一秒钟,我们不知道要获得的随机变量的种类。 我们的第一步通常是创建一个大样本,在这种情况下,将抛出 10,000 次,并生成带有结果的直方图:
In [3]: samples = D6.rvs(10000) + D6.rvs(10000) \
...: + D6.rvs(10000) + D100.rvs(10000)
In [4]: plt.hist(samples, bins=118-4); \
...: plt.xlabel('Sum of dice'); \
...: plt.ylabel('Frequency of each sum'); \
...: plt.show()
这给出了以下屏幕截图,清楚地表明我们的新随机变量不一致:

解决此问题的一种方法是根据此数据估算变量的分布,对于该任务,我们使用scipy.stats模块的函数gaussian_kde,该函数使用高斯内核执行内核密度估计:
In [5]: kernel = gaussian_kde(samples)
该gaussian_kde对象具有与实际随机变量相似的方法。 为了估计相应的获得 50 的概率的值以及在这四个骰子的掷骰中获得大于 100 的概率的值,我们将分别发布:
In [6]: kernel(50) # The actual answer is 1/100
Out[6]: array([ 0.00970843])
In [7]: kernel.integrate_box_1d(0,100) # The actual answer is 177/200
Out[7]: 0.88395064140531865
代替估计随机变量的总和,并再次假设我们对实际结果不熟悉,我们可以通过根据被加数的概率质量函数定义其概率质量函数来创建实际随机变量。 钥匙? 当然,卷积,因为这些骰子的随机变量是独立的。 样本空间是从 4 到 118 的一组数字(以下命令中的space_sum),并且与每个元素相关的概率(probs_sum)被计算为每个骰子在其样本空间上的对应概率的卷积 :
In [8]: probs_6dice = D6.pmf(np.linspace(1,6,6)); \
...: probs_100dice = D100.pmf(np.linspace(1,100,100))
In [9]: probs_sum = np.convolve(np.convolve(probs_6dice,probs_6dice),
...: np.convolve(probs_6dice,probs_100dice)); \
...: space_sum = np.linspace(4, 118, 115)
In [10]: sum_of_dice = rv_discrete(name="sod",
....: values=(space_sum, probs_sum))
In [11]: sum_of_dice.pmf(50)
Out[11]: 0.0099999999999999985
In [12]: sum_of_dice.cdf(100)
Out[12]: 0.89500000000000057
数据探索
数据探索通常是通过呈现其分布的有意义的综合来进行的-它可以通过一系列图形,通过使用一组数值参数进行描述或通过简单函数对其进行*似来实现。 现在,让我们探索不同的可能性,以及如何使用 SciPy 堆栈中的不同工具来实现它们。
用图表描绘分布
图的类型取决于变量的类型(分类,定量或日期)。
条形图和饼图
当我们用分类变量描述数据时,我们经常使用饼图或条形图来表示它。 例如,我们可以从消费者金融保护局的 http://catalog.data.gov/dataset/consumer-complaint-database 访问消费者投诉数据库。 该数据库于 2014 年 2 月创建,包含了无线电通信局收到的有关金融产品和服务的投诉。 在同年 3 月的更新版本中,它包含了自 2011 年 11 月以来获得的* 300,000 起投诉:
In [1]: import numpy as np, pandas as pd, matplotlib.pyplot as plt
In [2]: data = pd.read_csv("Consumer_Complaints.csv",
...: low_memory=False, parse_dates=[8,9])
In [3]: data.head()
Out[3]:
Complaint ID Product \
0 1015754 Debt collection
1 1015827 Debt collection
2 1016131 Debt collection
3 1015974 Bank account or service
4 1015831 Bank account or service
Sub-product \
0 Other (phone, health club, etc.)
1 NaN
2 Medical
3 Checking account
4 Checking account
Issue \
0 Cont'd attempts collect debt not owed
1 Improper contact or sharing of info
2 Disclosure verification of debt
3 Problems caused by my funds being low
4 Problems caused by my funds being low
Sub-issue State ZIP code \
0 Debt was paid NY 11433
1 Contacted me after I asked not to VT 5446
2 Right to dispute notice not received TX 77511
3 NaN FL 32162
4 NaN TX 77584
Submitted via Date received Date sent to company \
0 Web 2014-09-05 2014-09-05
1 Web 2014-09-05 2014-09-05
2 Web 2014-09-05 2014-09-05
3 Web 2014-09-05 2014-09-05
4 Web 2014-09-05 2014-09-05
Company \
0 Enhanced Recovery Company, LLC
1 Southwest Credit Systems, L.P.
2 Expert Global Solutions, Inc.
3 FNIS (Fidelity National Information Services, ...
4 JPMorgan Chase
Company response Timely response? \
0 Closed with non-monetary relief Yes
1 In progress Yes
2 In progress Yes
3 Closed with explanation Yes
4 Closed with explanation Yes
Consumer disputed?
0 NaN
1 NaN
2 NaN
3 NaN
4 NaN
提示
我们以他们提供的最简单格式(一个逗号分隔值的文件)下载了数据库。 我们使用命令read_csv从pandas进行操作。 如果要以其他格式(JSON,Excel 等)下载数据库,则只需相应地调整读取命令:
>>> pandas.read_csv("Consumer_Complaints.csv") # CSV
>>> pandas.read_json("Consumer_Complaints.json") # JSON
>>> pandas.read_excel("Consumer_Complaints.xls") # XLS
更令人惊讶的是,如果我们知道其 URL,则可以在线检索数据(无需将其保存到我们的计算机):
>>> url1 = "https://data.consumerfinance.gov/api/views"
>>> url2 = "/x94z-ydhh/rows.csv?accessType=DOWNLOAD"
>>> url = url1 + url2
>>> data = pd.read_csv(url)
如果数据库包含琐碎的数据,则解析器可能会与相应的dtype混淆。 在这种情况下,我们要求解析器尝试解决这种情况,但以使用更多的内存资源为代价。 我们可以通过包含可选的布尔标志low_memory=False来实现此目的,就像我们的运行示例一样。
另外,请注意我们如何指定parse_dates=True。 使用数据对文件进行的初步探索表明,第八列和第九列均代表日期。 库pandas具有强大的功能,无需借助复杂的str操作即可操作这些库,因此我们向读者指示将这些列转换为正确的格式。 这将减轻我们以后对数据的处理。
现在,让我们展示一个条形图,指示每个Product上每个公司有多少不同的投诉:
In [4]: data.groupby('Product').size()
Out[4]:
Product
Bank account or service 35442
Consumer loan 8187
Credit card 39114
Credit reporting 35761
Debt collection 37737
Money transfers 1341
Mortgage 118037
Payday loan 1228
Student loan 8659
dtype: int64
In [5]: _.plot(kind='barh'); \
...: plt.tight_layout(); \
...: plt.show()
这为我们提供了以下有趣的水*条形图,显示了从 2011 年 11 月到 2014 年 9 月每种产品的投诉量:

注意
pandas数据帧上的groupby方法等效于 SQL 中的GROUP BY。 有关熊猫中所有 SQL 命令及其等效数据框方法的完整说明,请在线访问 http://pandas.pydata.org/pandas-docs/stable/comparison_with_sql.html ,获取大量资源。
通过正确堆叠条形图,可以获得另一种有用的条形图。 例如,如果我们专注于 2012 年和 2013 年间有关中西部抵押贷款的投诉,则可以发出以下命令:
In [6]: midwest = ['ND', 'SD', 'NE', 'KS', 'MN', 'IA', \
'MO', 'IL', 'IN', 'OH', 'WI', 'MI']
In [7]: df = data[data.Product == 'Mortgage']; \
...: df['Year'] = df['Date received'].map(lambda t: t.year); \
...: df = df.groupby(['State','Year']).size(); \
...: df[midwest].unstack().ix[:, 2012:2013]
Out[7]:
Year 2012 2013
State
ND 14 20
SD 33 34
NE 109 120
KS 146 169
MN 478 626
IA 99 125
MO 519 627
IL 1176 1609
IN 306 412
OH 1047 1354
WI 418 523
MI 1457 1774
In [8]: _.plot(kind="bar", stacked=True, colormap='Greys'); \
...: plt.show()
该图清楚地说明了在这些州,2013 年是如何引起大量投诉的:

鉴于先前的图表,我们可能倾向于认为,在美国的每个州或地区,抵押贷款都是头号投诉。 从 2011 年 11 月到 2014 年 9 月,仅在波多黎各的饼图显示每种产品的投诉量就不同了:
In [9]: data[data.State=='PR'].groupby('Product').size()
Out[9]:
Product
Bank account or service 81
Consumer loan 20
Credit card 149
Credit reporting 139
Debt collection 62
Mortgage 110
Student loan 11
Name: Company, dtype: int64
In [10]: _.plot(kind='pie', shadow=True, autopct="%1.1f%%"); \
....: plt.axis('equal'); \
....: plt.tight_layout(); \
....: plt.show()
该图说明了信用卡和信用报告是这些岛上投诉的主要来源:

直方图
对于定量变量,我们使用直方图。 在上一节中,我们看到了由 10,000 个四骰子掷出构成直方图的示例。 在本节中,我们从pandas内生成另一个直方图。 在这种情况下,我们想提供一个直方图来分析每日关于信用卡的投诉与每日关于抵押的投诉的比率:
In [11]: df = data.groupby(['Date received', 'Product']).size(); \
....: df = df.unstack()
In [12]: ratios = df['Mortgage'] / df['Credit card']
In [13]: ratios.hist(bins=50); \
....: plt.show()
例如,结果图表明,在几天中,对抵押贷款的投诉数量约为信用卡投诉数量的 12 倍。 它还表明,最常见的情况是在几天中抵押贷款投诉数量大约是信用卡投诉数量的三倍:

时间图
对于随时间间隔测量的变量,我们使用时间图。 库pandas可以很好地处理这些问题。 例如,要观察从 2012 年 1 月 1 日到 2013 年 12 月 31 日每天收到的投诉量,我们发出以下命令:
In [14]: ts = data.groupby('Date received').size(); \
....: ts = ts['2012':'2013']; \
....: ts.plot(); \
....: plt.ylabel('Daily complaints'); \
....: plt.show()
请注意图表的振荡性质,以及在此期间投诉的轻微上升趋势。 我们还会观察到一些离群值-一个在 2012 年 3 月,一个在同年 5 月至 6 月之间,以及在 2013 年 1 月和 2013 年 2 月:

用数字和箱形图描述分布
我们要求每个数据集使用常规参数:
- *均值(算术,几何或谐波)和中位数以测量数据中心
- 四分位数,方差,标准差或均值的标准误,用于测量数据的传播
- 中心矩,偏度和峰度用于测量数据分布中的对称程度
- 在数据中查找最常见值的模式
- 先前参数的修整版,可以更好地描述数据分布,从而减少离群值的影响
呈现上述某些信息的一种好方法是借助五位数摘要或带箱线图。
让我们说明如何在pandas(左列)和scipy.stats库(右列)中实现这些基本测量:
In [15]: ts.describe() In [16]: import scipy.stats
Out[15]:
count 731.000000 In [17]: scipy.stats.describe(ts)
mean 247.333789 Out[17]:
std 147.02033 (731,
min 9.000000 (9, 628),
25% 101.000000 247.33378932968537,
50% 267.000000 21614.97884301858,
75% 364.000000 0.012578861206579875,
max 628.000000 -1.1717653990872499)
dtype: float64
第二个输出为我们提供计数(731 = 366 + 365),最小值和最大值(min=9,max=628),算术*均值(247),无偏方差(21614),偏斜度(0.0126) 和偏峰度(-1.1718)。
使用pandas(众数和标准偏差)和scipy.stats(50 和 600 之间的所有值的均值和修整方差的标准误差)进行的其他参数计算:
In [18]: ts.mode() In [20]: scipy.stats.sem(ts)
Out[18]: Out[20]: 5.4377435122312807
0 59
dtype: int64 In [21]: scipy.stats.tvar(ts, [50, 600])
Out[21]: 17602.318623850999
In [19]: ts.std()
Out[19]: 147.02033479426774
提示
有关scipy.stats库中所有统计功能的完整说明,最好的参考是 http://docs.scipy.org/doc/scipy/reference/stats.html 上的官方文档。
在参数计算中可以忽略NaN值。 熊猫中的大多数数据框和序列方法都会自动执行此操作。 如果需要不同的行为,我们可以用那些NaN值替代我们认为适当的任何值。 例如,如果我们希望以前的任何计算都考虑所有日期,而不仅考虑注册的日期,则可以在这些事件中将零投诉的值强加于人。 我们使用dataframe.fillna(0)方法执行此操作。
使用库scipy.stats,如果我们想忽略数组中的NaN值,我们可以使用相同的例程在名称前附加关键字nan:
>>> scipy.stats.nanmedian(ts)
267.0
无论如何,我们计算出的时间序列显示绝对没有NaN -在 2012 年和 2013 年,每天每天至少有 9 笔日常财务投诉。
回到中西部地区有关抵押贷款的投诉,以说明箱线图的威力,我们将调查每个州在 2013 年每月对抵押贷款的投诉数量:
In [22]: in_midwest = data.State.map(lambda t: t in midwest); \
....: mortgages = data.Product == 'Mortgage'; \
....: in_2013 =data['Date received'].map(lambda t: t.year==2013);\
....: df = data[mortgages & in_2013 & in_midwest]; \
....: df['month'] = df['Date received'].map(lambda t: t.month); \
....: df = df.groupby(['month', 'State']).size(); \
....: df.unstack()
Out[22]:
State IA IL IN KS MI MN MO ND NE OH SD WI
month
1 11 220 40 12 183 99 91 3 13 163 5 58
2 14 160 37 16 180 45 47 2 12 120 NaN 37
3 7 138 43 18 184 52 57 3 11 131 5 50
4 14 148 33 19 185 55 52 2 14 104 3 48
5 14 128 44 16 172 63 57 2 8 109 3 43
6 20 136 47 13 164 51 47 NaN 13 116 7 52
7 5 127 30 16 130 57 62 2 11 127 5 39
8 11 133 32 15 155 64 55 NaN 8 120 1 51
9 10 121 24 16 99 31 55 NaN 8 109 NaN 37
10 9 96 35 12 119 50 37 3 10 83 NaN 35
11 4 104 22 10 96 22 39 2 6 82 3 32
12 6 98 25 6 107 37 28 1 6 90 2 41
In [23]: _.boxplot(); \
....: plt.show()
该箱线图说明了在中西部各州(伊利诺伊州,俄亥俄州和密歇根州)每月抵押贷款投诉最多的情况。 例如,以密歇根州(MI)为例,相应的箱线图表明点差从 96 上升至 185 每月投诉。 该州每月投诉的中位数约为 160。第一和第三四分位数分别为 116 和 180:

小提琴图是一个箱形图,每侧都有旋转的核密度估计。 这显示了不同值的数据的概率密度。 我们可以从statsmodels子模块graphics.boxplots使用图形例程violinplot获得这些图。 让我们用与之前相同的数据来说明这种图:
提示
另一种选择是将小提琴图与所有单个数据点的线散布图结合起来。 我们将其称为 bean 图,并且在同一子模块statsmodels.graphics.boxplot中具有例程beanplot的实现。
In [24]: from statsmodels.graphics.boxplots import violinplot
In [25]: df = df.unstack().fillna(0)
In [26]: violinplot(df.values, labels=df.columns); \
....: plt.show()

定量变量之间的关系
为了表达两个定量变量之间的关系,我们采用三种技术:
- 散点图,以可视方式识别该关系
- 相关系数的计算,表示通过线性函数表示该关系的可能性
- 回归功能是一种预测变量相对于另一个变量的值的方法
散点图和关联
例如,我们将尝试在四个人口稠密的州(伊利诺伊州,纽约州,得克萨斯州和加利福尼亚州)和波多黎各的领土之间找到有关抵押贷款投诉数量的任何关系。 我们将比较 2011 年 12 月至 2014 年 9 月期间每个月的投诉数量:
In [27]: from pandas.tools.plotting import scatter_matrix
In [28]: def year_month(t):
....: return t.tsrftime("%Y%m")
....:
In [29]: states = ['PR', 'IL', 'NY', 'TX', 'CA']; \
....: states = data.State.map(lambda t: t in states); \
....: df = data[states & mortgages]; \
....: df['month'] = df['Date received'].map(year_month); \
....: df.groupby(['month', 'State']).size().unstack()
Out[29]:
State CA IL NY PR TX
month
2011/12 288 34 90 7 63
2012/01 444 77 90 2 104
2012/02 446 80 110 3 115
2012/03 605 78 179 3 128
2012/04 527 69 188 5 152
2012/05 782 100 242 3 151
2012/06 700 107 204 NaN 153
2012/07 668 114 198 3 153
2012/08 764 108 228 3 187
2012/09 599 92 192 1 140
2012/10 635 125 188 2 150
2012/11 599 99 145 6 130
2012/12 640 127 219 2 128
2013/01 1126 220 342 3 267
2013/02 928 160 256 4 210
2013/03 872 138 270 1 181
2013/04 865 148 254 5 200
2013/05 820 128 242 4 198
2013/06 748 136 232 1 237
2013/07 824 127 258 5 193
2013/08 742 133 236 3 183
2013/09 578 121 203 NaN 178
2013/10 533 96 193 2 123
2013/11 517 104 173 1 141
2013/12 463 98 163 4 152
2014/01 580 80 201 3 207
2014/02 670 151 189 4 189
2014/03 704 141 245 4 182
2014/04 724 146 271 4 212
2014/05 559 110 212 10 175
2014/06 480 107 226 6 141
2014/07 634 113 237 1 171
2014/08 408 105 166 5 118
2014/09 1 NaN 1 NaN 1
In [30]: df = _.dropna(); \
....: scatter_matrix(df); \
....: plt.show()
这给出了每个状态对数据之间的散点图矩阵,以及每个状态下相同数据的直方图:

对于添加了置信椭圆的散点图网格,我们可以使用软件包statsmodels的图形模块graphics.plot_grids中的例程scatter_ellipse:
In [31]: from statsmodels.graphics.plot_grids import scatter_ellipse
In [32]: scatter_ellipse(df, varnames=df.columns); \
....: plt.show()

请注意,每个图像如何附带额外的信息。 这是两个变量(在这种情况下为皮尔森氏)的相应相关系数。 看起来几乎完全对齐的数据的相关系数的绝对值非常接* 1。 我们还可以通过pandas数据帧方法corr获得所有这些系数:
In [33]: df.corr(method="pearson")
Out[33]:
State CA IL NY PR TX
State
CA 1.000000 0.844015 0.874480 -0.210216 0.831462
IL 0.844015 1.000000 0.818283 -0.141212 0.805006
NY 0.874480 0.818283 1.000000 -0.114270 0.837508
PR -0.210216 -0.141212 -0.114270 1.000000 -0.107182
TX 0.831462 0.805006 0.837508 -0.107182 1.000000
提示
除了标准的 Pearson 相关系数之外,该方法还允许我们为序数数据(kendall)或 Spearman 排名(spearman)计算 Kendall Tau:
在模块scipy.stats中,我们还具有用于计算这些相关系数的例程。
pearsonr用于 Pearson 相关系数和用于测试非相关性的 p 值spearmanr用于 Spearman 等级相关系数和 p 值以测试不相关kendalltau代表肯德尔的牛头pointbiserial表示点双数相关系数和关联的 p 值
视觉上显示相关性的另一种可能性是借助于颜色网格。 子模块statsmodels.graphics.correlation的图形例程plot_corr完成工作:
In [34]: from statsmodels.graphics.correlation import plot_corr
In [35]: plot_corr(df.corr(method='spearman'),
....: xnames=df.columns.tolist()); \
....: plt.show()

最大的关联发生在纽约州和加利福尼亚州之间(0.874480)。 在下一节中,我们将使用与这两种状态相对应的数据作为后续示例。
回归
散点图帮助我们确定了可能通过功能关系关联数据的情况。 这使我们能够制定规则以预测变量的值,而该变量会相互了解。 当我们怀疑存在这样的公式时,我们想找到一个很好的*似值。
在本章中,我们遵循统计学家和数据分析师的行话,因此,我们将其称为回归,而不是将其称为*似。 我们还附加了一个形容词,指示我们寻求的公式的类型。 这样,如果函数是线性的,我们就称为线性回归;如果函数是多项式的,我们就称为多项式回归,依此类推。 同样,就另一单个变量而言,回归并不一定只涉及一个变量。 因此,我们区分了单变量回归和多元回归。 让我们探索用于回归的不同设置,以及如何从 SciPy 堆栈中解决它们。
中型数据集的普通线性回归
在任何给定情况下,我们都可以使用在最小二乘意义上的*似和插值探索中学到的工具,这些知识在第 1 章,“数值线性代数”和中 第 2 章和“插值和*似”。 两个库scipy.stack和statsmodels中以及工具包scikit-learn中都有许多工具可以执行此操作和相关分析:
- 在
scipy.stats库中计算普通最小二乘回归线linregress的基本例程。 scikit-learn工具包中的LinearRegression类,位于sklearn.linear_model。- 在
patsy包的帮助下,statsmodel库中有一组不同的回归例程。
让我们从scipy.stats库中通过linregress的最简单方法开始。 我们想探索加利福尼亚州和纽约州每月对抵押贷款的投诉数量之间的几乎线性关系:
In [36]: x, y = df[['NY', 'CA']].T.values
In [37]: slope,intercept,r,p,std_err = scipy.stats.linregress(x,y); \
....: print "Formula: CA = {0} + {1}*NY".format(intercept, slope)
Formula: CA = 65.7706648926 + 2.82130682025*NY
In [38]: df[['NY', 'CA']].plot(kind='scatter', x='NY', y='CA'); \
....: xspan = np.linspace(x.min(), x.max()); \
....: plt.plot(xspan, intercept + slope * xspan, 'r-', lw=2); \
....: plt.show()

这与通过使用scikit-learn工具包中的LinearRegression获得的结果完全相同:
In [39]: from sklearn.linear_model import LinearRegression
In [40]: model = LinearRegression()
In [41]: x = np.resize(x, (x.size, 1))
In [42]: model.fit(x, y)
Out[42]: LinearRegression(copy_X=True, fit_intercept=True,
....: normalize=False)
In [43]: model.intercept_
Out[43]: 65.770664892647233
In [44]: model.coef_
Out[44]: array([ 2.82130682])
要对该普通最小二乘回归线进行更高级的处理,以提供更多的信息图和摘要,我们使用statsmodels中的例程ols以及其一些令人敬畏的绘图工具:
In [45]: import statsmodels.api as sm; \
....: from statsmodels.formula.api import ols
In [46]: model = ols("CA ~ NY", data=df).fit()
In [47]: print model.summary2()
Results: Ordinary least squares
==========================================================
Model: OLS AIC: 366.3982
Dependent Variable: CA BIC: 369.2662
No. Observations: 31 Log-Likelihood: -181.20
Df Model: 1 F-statistic: 94.26
Df Residuals: 29 Prob (F-statistic): 1.29e-10
R-squared: 0.765 Scale: 7473.2
Adj. R-squared: 0.757
----------------------------------------------------------
Coef. Std.Err. t P>|t| [0.025 0.975]
----------------------------------------------------------
Intercept 65.7707 62.2894 1.0559 0.2997 -61.6254 193.1667
NY 2.8213 0.2906 9.7085 0.0000 2.2270 3.4157
----------------------------------------------------------
Omnibus: 1.502 Durbin-Watson: 0.921
Prob(Omnibus): 0.472 Jarque-Bera (JB): 1.158
Skew: -0.465 Prob(JB): 0.560
Kurtosis: 2.823 Condition No.: 860
==========================================================
提示
一种有趣的方法来表达一个事实,即我们想要获得一个相对于变量NY: CA ~ NY的变量CA的公式。 归功于patsy库,该舒适的语法成为可能,该库负责进行所有相关的解释并在后台处理相应的数据。
可以使用子模块statsmodels.graphics.regressionplots中的图形例程plot_fit来显示拟合:
In [48]: from statsmodels.graphics.regressionplots import plot_fit
In [49]: plot_fit(model, 'NY'); \
....: plt.show()

让我们还说明如何使用statsmodels进行多元线性回归。 对于以下示例,我们将在 2013 年收集我们怀疑与之相关的三种产品的投诉数量。 我们将尝试找到一个公式,将抵押贷款投诉的总数与信用卡和学生贷款的投诉数量进行*似计算:
In [50]: products = ['Student loan', 'Credit card', 'Mortgage']; \
....: products = data.Product.map(lambda t: t in products); \
....: df = data[products & in_2013]; \
....: df = df.groupby(['State', 'Product']).size()
....: df = df.unstack().dropna()
In [51]: X = df[['Credit card', 'Student loan']]; \
....: X = sm.add_constant(X); \
....: y = df['Mortgage']
In [52]: model = sm.OLS(y, X).fit(); \
....: print model.summary2()
Results: Ordinary least squares
===============================================================
Model: OLS AIC: 827.7591
Dependent Variable: Mortgage BIC: 833.7811
No. Observations: 55 Log-Likelihood: -410.88
Df Model: 2 F-statistic: 286.6
Df Residuals: 52 Prob (F-statistic): 8.30e-29
R-squared: 0.917 Scale: 1.9086e+05
Adj. R-squared: 0.914
---------------------------------------------------------------
Coef. Std.Err. t P>|t| [0.025 0.975]
---------------------------------------------------------------
const 2.1214 77.3360 0.0274 0.9782 -153.0648 157.3075
Credit card 6.0196 0.5020 11.9903 0.0000 5.0121 7.0270
Student loan -9.9299 2.5666 -3.8688 0.0003 -15.0802 -4.7796
---------------------------------------------------------------
Omnibus: 20.251 Durbin-Watson: 1.808
Prob(Omnibus): 0.000 Jarque-Bera (JB): 130.259
Skew: -0.399 Prob(JB): 0.000
Kurtosis: 10.497 Condition No.: 544
===============================================================
注意r-squared的值,所以要接*1。 这表明已经计算出线性公式,并且相应的模型很好地拟合了数据。 现在,我们可以生成一个可视化显示它:
In [53]: from mpl_toolkits.mplot3d import Axes3D
In [54]: xspan = np.linspace(X['Credit card'].min(),
....: X['Credit card'].max()); \
....: yspan = np.linspace(X['Student loan'].min(),
....: X['Student loan'].max()); \
....: xspan, yspan = np.meshgrid(xspan, yspan); \
....: Z = model.params[0] + model.params[1] * xspan + \
....: model.params[2] * yspan; \
....: resid = model.resid
In [55]: fig = plt.figure(figsize=(8, 8)); \
....: ax = Axes3D(fig, azim=-100, elev=15); \
....: surf = ax.plot_surface(xspan, yspan, Z, cmap=plt.cm.Greys,
....: alpha=0.6, linewidth=0); \
....: ax.scatter(X[resid>=0]['Credit card'],
....: X[resid>=0]['Student loan'],
....: y[resid >=0],
....: color = 'black', alpha=1.0, facecolor='white'); \
....: ax.scatter(X[resid<0]['Credit card'],
....: X[resid<0]['Student loan'],
....: y[resid<0],
....: color='black', alpha=1.0); \
....: ax.set_xlabel('Credit cards'); \
....: ax.set_ylabel('Student loans'); \
....: ax.set_zlabel('Mortgages'); \
....: plt.show()
相应的图以白色显示*面上方的数据点,以黑色显示*面下方的数据点。 *面的强度由抵押投诉数量的相应预测值确定(较亮的区域等于低的残差,较暗的区域等于高的残差)。

大型数据集的普通最小二乘回归
对于大型数据集(超过 100,000 个样本)的线性回归,最优算法使用随机梯度下降( SGD )回归学习,并具有不同的损失函数和损失。 我们可以使用sklearn.linear_model中的SGDregressor类访问它们。
超出普通最小二乘法的线性回归
在多元线性回归的一般情况下,如果不着重于特定变量集,我们将采用岭回归。 我们通过scikit-learn工具包中的sklearn.linear_model.Ridge类进行操作。
Ridge 回归基本上是一种普通的最小二乘算法,对涉及的系数大小有额外的惩罚。 它的性能也可以与普通最小二乘法相媲美,因为它们的复杂度大致相同。
在任何给定的线性回归中,如果我们承认只有很少的变量对整体回归有很大影响,则首选方法是最小绝对收缩和选择算子( LASSO )和弹性网。 当样本数量远大于变量数量时,我们选择套索,并寻找一个稀疏公式,其中与非重要变量相关的大多数系数为零。
当变量数量接*或大于样本数量时,弹性网始终是首选算法。
两种方法都可以通过scikit-learn-sklearn.linear_model.lasso和sklearn.linear_model.ElasticNet类实现。
支持向量机
支持向量回归是另一种强大的算法,其前提是训练数据的子集对整个变量集有很大影响。 这种解决不*衡问题的方法的优势在于,只需将算法中的核函数更改为决策函数,我们就可以访问不同类型的回归(不仅是线性回归)。 但是,当变量多于样本时,这不是一个好选择。
在scipy-learn工具包中,我们有两个不同的类来实现此算法的变体:svm.SCV和svm.NuSVC。 带有线性核的svm.SVC的简化变体可以称为svm.LinearSVC类。
设定方法
当其他所有方法都失败时,我们有几种算法结合了几种基本估计量的功能。 这些算法分为两个大家族:
- *均方法:此方法通过*均来组合估计量,以减少残差方差的值
- 增强方法:这建立了一系列弱估计量,这些估计量收敛到回归而没有任何偏差
有关通过scikit-learn工具包对这些方法,示例和实现的详细说明,请参阅上的官方文档,网址为 http://scikit-learn.org/stable/modules/ensemble.html 。
时间序列分析
时间序列建模和分析的子领域也非常丰富。 从企业业务/行业指标(经济预测,股票市场分析,效用研究等)到生物过程(流行病学动态,人口研究等),这种数据出现在许多过程中。 建模和分析背后的思想是,随着时间的推移获取的数据可能具有某种基础结构,例如,对于预测目的,跟踪该趋势是理想的。
我们使用 Box-Jenkins 模型(也称为自回归综合移动*均值模型,或简称为 ARIMA )将系列的现值与 过去的值和过去的预测错误。 在包statsmodels中,我们通过子模块statsmodels.tsa中的以下类实现:
-
为了描述自回归模型 AR(p),我们使用类
ar_model.AR或等效地为arima_model.AR。 -
为了描述自回归移动*均值模型 ARMA(p,q),我们采用了
arima_model.ARMA类。 卡尔曼滤波器的实现可帮助 ARIMA 模型。 我们用kalmanf.kalmanfilter.KalmanFilter(同样在statsmodels.tsa子模块中)调用此代码。 -
For the general ARIMA model ARIMA(p,d,q), we use the classes
arima_model.ARIMA.提示
可以使用不同类型的模型根据其自身的历史预测时间序列-使用滞后和差异的回归模型,随机游走模型,指数*滑模型和季节性调整。 实际上,所有这些类型的模型都是 Box-Jenkins 模型的特例。
一个基本的例子就足够了。 回想一下我们通过收集 2013 年和 2014 年所有日常投诉创建的时间序列。从其图上可以明显看出,我们的时间序列ts不是季节性的,也不是*稳的(有轻微的上升趋势)。 该系列过于尖锐,无法进行舒适的分析。 在进行任何描述之前,我们会每周对其进行重新采样。 对于此任务,我们使用pandas时间序列的重采样方法:
In [56]: ts = ts.resample('W')
In [57]: ts.plot(); \
....: plt.show()

使用第一个或第二个差异去趋势化–这表明我们必须使用带有d=1或d=2的 ARIMA(p,d,q)模型来描述它。
让我们计算和可视化第一个差异序列ts[date] - ts[date-1]:
提示
在pandas中,我们有一个很好的方法来计算任何时间段之间的差异,例如ts.diff(periods=k)。
In [58]: ts_1st_diff = ts.diff(periods=1)[1:]
In [59]: ts_1st_diff.plot(marker='o'); \
....: plt.show()

确实看起来像是*稳的系列。 因此,我们将为 ARIMA 模型选择d=1。 接下来,是该新时间序列的相关图的可视化。 我们有两种方法来执行此任务:pandas中的基本自相关和滞后图,以及statsmodels.api.graphics.tsa中的一组更复杂的相关图:
In [60]: from pandas.tools.plotting import autocorrelation_plot, \
....: lag_plot
In [61]: autocorrelation_plot(ts_1st_diff); \
....: plt.show()
这给出了下图,显示了在不同时滞(在这种情况下,从 0 到 1000 天)中数据与自身的相关性。 黑色实线对应于 95%置信带,虚线对应于 99%置信带:

在非随机时间序列的情况下,一个或多个自相关将明显为非零。 请注意,在我们的情况下,此图对于随机性是一个很好的情况。
滞后图加强了这种观点:
In [62]: lag_plot(ts_1st_diff, lag=1); \
....: plt.axis('equal'); \
....: plt.show()
在这种情况下,我们选择了一天的滞后图,该图既不显示对称也不显示明显的结构:

statsmodels库通过其子模块tsa和api.graphics.tsa擅长处理时间序列。 例如,要像以前一样执行自相关,但这次将延迟限制为 40,我们发出sm.graphics.tsa.plot_acf。 我们可以使用以下命令:
In [63]: fig = plt.figure(); \
....: ax1 = fig.add_subplot(211); \
....: ax2 = fig.add_subplot(212); \
....: sm.graphics.tsa.plot_acf(ts_1st_diff, lags=40,
....: alpha=0.05, ax=ax1); \
....: sm.graphics.tsa.plot_pacf(ts_1st_diff, lags=40,
....: alpha=0.05, ax=ax2); \
....: plt.show()
这是呈现自相关函数的另一种方式,但同样有效。 注意我们如何控制置信带。 我们对 alpha 的选择决定了其含义-例如,在这种情况下,通过选择alpha=0.05,我们强加了 95%的置信带。

为了将相应的值呈现给计算出的自相关,我们使用子模块statsmodels.tsa.stattools中的例程acf和pacf:
In [64]: from statsmodels.tsa.stattools import acf, pacf
In [65]: acf(ts_1st_diff, nlags=40)
Out[65]:
array([ 1\. , -0.38636166, -0.16209701, 0.13397057,
-0.0555708 , 0.05048394, -0.0407119 , -0.02082811,
0.0040006 , 0.02907198, 0.04330283, -0.02011055,
-0.01537464, -0.02978855, 0.04849505, 0.01825439,
-0.02593023, -0.07966487, 0.02102888, 0.10951272,
-0.10171504, -0.00645926, 0.03973507, 0.03865624,
-0.12395291, 0.03391616, 0.07447618, -0.02474901,
-0.01742892, -0.02676263, -0.00276295, 0.03135769,
0.0155686 , -0.09556651, 0.07881427, 0.04804349,
-0.03797063, -0.05942366, 0.03913402, -0.00854744, -
0.03463874])
In [66]: pacf(ts_1st_diff, nlags=40)
Out[66]:
array([ 1\. , -0.39007667, -0.37436812, -0.13265923,
-0.14290863, -0.00457552, -0.05168091, -0.05241386,
-0.07909324, -0.01776889, 0.06631977, 0.07931566,
0.0567529 , -0.02606054, 0.02271939, 0.05509316,
0.06013166, -0.09309867, -0.11283787, 0.03704051,
-0.06223677, -0.05993707, -0.03659954, 0.07764279,
-0.16189567, -0.11602938, 0.00638008, 0.09157757,
0.04046057, -0.04838127, -0.08806197, -0.02527639,
0.06392126, -0.13768596, 0.00641743, 0.11618549,
0.12550781, -0.14070774, -0.05995693, -0.0024937 ,
-0.0905665 ])
鉴于先前计算的相关图,我们有一个MA(1)签名; ACF 图中有一个负尖峰,而 PACF 图中有一个衰减模式(从下方)。 ARIMA 模型的一个明智的参数选择是p=0,q=1和d=1(这对应于一个简单的指数*滑模型,可能添加了一个常数项)。 然后,使用以下选择进行模型描述和进一步的预测:
In [67]: from statsmodels.tsa import arima_model
In [68]: model = arima_model.ARIMA(ts, order=(0,1,1)).fit()
在运行时,此代码通知我们有关其实现的一些详细信息:
RUNNING THE L-BFGS-B CODE
* * *
Machine precision = 2.220D-16
N = 2 M = 12
This problem is unconstrained.
At X0 0 variables are exactly at the bounds
At iterate 0 f= 5.56183D+02 |proj g|= 2.27373D+00
At iterate 5 f= 5.55942D+02 |proj g|= 1.74759D-01
At iterate 10 f= 5.55940D+02 |proj g|= 0.00000D+00
* * *
Tit = total number of iterations
Tnf = total number of function evaluations
Tnint = total number of segments explored during Cauchy searches
Skip = number of BFGS updates skipped
Nact = number of active bounds at final generalized Cauchy point
Projg = norm of the final projected gradient
F = final function value
* * *
N Tit Tnf Tnint Skip Nact Projg F
2 10 12 1 0 0 0.000D+00 5.559D+02
F = 555.940373727761
CONVERGENCE: NORM_OF_PROJECTED_GRADIENT_<=_PGTOL
Cauchy time 0.000E+00 seconds.
Subspace minimization time 0.000E+00 seconds.
Line search time 0.000E+00 seconds.
Total User time 0.000E+00 seconds.
类arima_model.ARIMA的方法fit在statsmodels.tsa,arima_model_ARIMAResults中创建了一个新类,其中包含我们需要的所有信息以及一些提取方法:
In [69]: print model.summary()

让我们观察一下残差的相关图。 我们可以使用对象模型的方法resid计算这些值:
In [70]: residuals = model.resid
In [71]: fig = plt.figure(); \
....: ax1 = fig.add_subplot(211); \
....: ax2 = fig.add_subplot(212); \
....: sm.graphics.tsa.plot_acf(residuals, lags=40,
....: alpha=0.05, ax=ax1); \
....: ax1.set_title('Autocorrelation of the residuals of the ARIMA(0,1,1) model'); \
....: sm.graphics.tsa.plot_pacf(residuals, lags=40,
....: alpha=0.05, ax=ax2); \
....: ax2.set_title('Partial Autocorrelation of the residuals of the ARIMA(0,1,1) model'); \
....: plt.show()

这些图表明我们选择了一个好的模型。 剩下的只是使用它来产生预测。 为此,我们在对象模型中采用了方法predict。 例如,通过考虑自 2013 年 10 月以来的所有数据进行的 2014 年前几周的预测可以如下执行:
In [72]: np.where((ts.index.year==2013) & (ts.index.month==10))
Out[72]: (array([92, 93, 94, 95]),)
In [73]: prediction = model.predict(start=92, end='1/15/2014')
In [74]: prediction['10/2013':].plot(lw=2, label='forecast'); \
....: ts_1st_diff['9/2013':].plot(style='--', color='red',
....: label='True data (first differences)'); \
....: plt.legend(); \
....: plt.show()
这给了我们以下预测:

摘要
到此为止,我们分两章介绍了数据分析的第一部分,其中我们探索了 SciPy 堆栈中用于计算和描述性统计的可视化的高级 Python 工具。 在下一章中,我们对推断统计,数据挖掘和机器学习进行类似的处理。
八、推断和数据分析
上一章介绍的各种描述性统计技术使我们可以直接从数据中展示事实。 下一个逻辑步骤是推论-提出主张并得出比样本数据所代表的人口更大的结论的过程。
本章将涵盖以下主题:
- 统计推断。
- 数据挖掘和机器学习。
统计推断
统计推断是通过数据分析来推断基础分布的属性的过程。 推断统计分析可推断总体的属性; 这包括检验假设和推导估计。
有三种类型的推断:
- 参数最合适的单个值的估计。
- 间隔估计,用于评估参数值的哪个区域与给定数据最一致。
- 假设检验,以确定在两个选项之间哪个参数值与数据最一致。
解决这些问题的方法主要有三种:
- Frequentist:根据重复采样的性能来判断推断。
- 贝叶斯:推断必须是主观的。 为我们寻求的参数选择一个先验分布,然后在获得联合分布之前将数据的密度合并起来。 贝叶斯定理的进一步应用为我们提供了给定数据的参数分布。 要在此设置下执行计算,我们使用包
PyMC。 - 可能性:推论基于以下事实:有关参数的所有信息都可以通过检查与概率密度函数成比例的似然函数来获得。
在本节中,我们简要说明三种推断类型的三种方法。 我们回到前面的例子中,每天抵押贷款对信用卡的投诉比率:
In [1]: import numpy as np, pandas as pd, matplotlib.pyplot as plt
In [2]: data = pd.read_csv("Consumer_Complaints.csv", \
...: low_memory=False, parse_dates=[8,9])
In [3]: df = data.groupby(['Date received', 'Product']).size(); \
...: df = df.unstack(); \
...: ratios = df['Mortgage'] / df['Credit card']
In [4]: ratios.describe()
Out[4]:
count 1001.000000
mean 2.939686
std 1.341827
min 0.203947
25% 1.985507
50% 2.806452
75% 3.729167
max 12.250000
dtype: float64
通过直方图的目视检查,我们可以很好地假设此数据是来自具有参数mu(*均值)和sigma(标准偏差)的正态分布的随机样本。 为了简单起见,我们进一步假设比例参数sigma是已知的,并且其值为1.3。
提示
在本节的稍后部分,我们将实际探索 SciPy 堆栈中拥有哪些工具来更精确地确定数据的分布。
参数估计
在这种情况下,我们要解决的问题是使用获得的数据估算*均值mu。
惯常做法
这是最简单的设置。 常用方法使用估计的数据*均值:
In [5]: ratios.mean()
Out[5]: 2.9396857495543731
In [6]: from scipy.stats import sem # Standard error
In [7]: sem(ratios.dropna())
Out[7]: 0.042411109594665049
提示
然后,常客会说:参数 mu 的估计值为 2.9396857495543731 ,标准误差为 0.042411109594665049。
贝叶斯方法
对于贝叶斯方法,我们为mu选择一个先验分布,我们可以方便地假定它是具有标准偏差1.3的正态分布。 *均值mu被视为变量,最初,我们假定其值可以在数据范围内的任何位置(具有均匀分布)。 然后,我们使用贝叶斯定理为mu计算后验分布。 然后,我们估计的参数是mu的后验分布的*均值:
In [8]: import pymc as pm
In [9]: mu = pm.Uniform('mu', lower=ratios.min(), upper=ratios.max())
In [10]: observation = pm.Normal('obs', mu=mu, tau=1./1.3**2,
....: value=ratios.dropna(), observed=True)
In [11]: model = pm.Model([observation, mu])
注意
请注意,在PyMC中,正态分布的定义如何需要*均参数mu,但是期望标准精度tau = 1/sigma**2而不是标准偏差或方差。
变量observation通过选项value=ratios.dropna()将我们的数据与我们建议的数据生成方案结合,该方案由变量mu给出。 为了确保在分析过程中保持不变,我们施加observed=True。
在学习步骤中,我们使用马氏链蒙特卡洛( MCMC )方法返回大量随机变量,用于mu的后验分布:
In [12]: mcmc = pm.MCMC(model)
In [13]: mcmc.sample(40000, 10000, 1)
[---------------100%---------------] 40000 of 40000 complete in 4.5 sec
In [14]: mcmc.stats()
Out[14]:
{'mu': {'95% HPD interval': array([ 2.86064764, 3.02292213]),
'mc error': 0.00028222883254203107,
'mean': 2.9396811517572554,
'n': 30000,
'quantiles': {2.5: 2.8589908555161485,
25: 2.9117191652137464,
50: 2.9396815504225815,
75: 2.9675088640073439,
97.5: 3.0216312862055279},
'standard deviation': 0.041412844137324857}}
In [15]: mcmc.summary()
Out[15]:
mu:
Mean SD MC Error 95% HPD interval
------------------------------------------------------------------
2.94 0.041 0.0 [ 2.861 3.023]
Posterior quantiles:
2.5 25 50 75 97.5
|---------------|===============|===============|---------------|
2.859 2.912 2.94 2.968 3.022
In [16]: from pymc.Matplot import plot as mcplot
In [17]: mcplot(mcmc); \
....: plt.show()
Plotting mu
我们应该得到类似于以下的输出:

参数的估计值为2.93968。 mu的后验分布的标准偏差为0.0414。
可能性方法
我们有一个方便的方法来执行似然法,以估计子模块scipy.stats中表示为类的任何分布的参数。 在我们的例子中,由于我们要固定标准偏差(scale作为该特定类的正态分布参数),我们将发出以下命令:
In [18]: from scipy.stats import norm as NormalDistribution
In [19]: NormalDistribution.fit(ratios.dropna(), fscale=1.3)
Out[19]: (2.9396857495543736, 1.3)
这为我们提供了相似的均值。 mu的(非负对数)似然函数图可以如下获得:
In [20]: nnlf = lambda t: NormalDistribution.nnlf([t, 1.3],
....: ratios.dropna()); \
....: nnlf = np.vectorize(nnlf)
In [21]: x = np.linspace(0, 14); \
....: plt.plot(x, nnlf(x), lw=2, color='r',
....: label='Non-negative log-likely function for $\mu$'); \
....: plt.legend(); \
....: plt.annotate('Minimum', xy=(2.9, nnlf(2.9)), xytext=(0,20),
....: textcoords='offset points', ha='right', va='bottom',
....: bbox=dict(boxstyle='round,pad=0.5', fc='yellow',
....: color='k', alpha=1),
....: arrowprops=dict(arrowstyle='->', color='k',
....: connectionstyle='arc3,rad=0')); \
....: plt.show()
我们应该得到类似于以下的输出:

无论如何,结果在视觉上是我们期望的:
In [22]: distribution = NormalDistribution(loc=2.9396857495543736,
....: scale=1.3)
In [23]: plt.plot(x, distribution.pdf(x), 'r-', lw=2,
....: label='Computed Probability Density Function'); \
....: ratios.hist(bins=50, alpha=0.2, normed=True,
....: label='Histogram of data (normalized)'); \
....: plt.legend(); \
....: plt.show()
我们应该得到类似于以下的输出:

间隔估算
在此设置中,我们寻求数据支持的mu值的间隔。
惯常做法
在常识性方法中,我们从提供一个小的置信系数 alpha开始,然后继续寻找一个间隔,以使包含参数mu的概率为1-alpha。 在我们的示例中,我们设置alpha = 0.05(因此,施加的概率为 95%),然后使用定义在模块scipy.stats中的连续分布的任何类别的方法interval来计算间隔:
In [24]: loc = ratios.mean(); \
....: scale = ratios.sem(); \
....: NormalDistribution.interval(0.95, scale=scale, loc=loc)
Out[24]: (2.8565615022044484, 3.0228099969042979)
根据该方法,2.8565615022044484和3.0228099969042979之间的*均值mu的值与基于 95%置信区间的数据一致。
贝叶斯方法
在贝叶斯方法中,等效于置信区间的区域称为可信区域(或区间),并且与最高后验密度区域相关联。 一组参数的最可能值,这些值合计构成后部质量的100*(1 - alpha)百分比。
回想一下,当使用 MCMC 进行采样时,我们获得了alpha = 0.05的可靠区域。
为了获得其他 alpha 值的可信区间,我们直接在子模块pymc.utils中使用例程hpd。 例如,alpha = 0.01的最高后验密度区域计算如下:
In [25]: pm.utils.hpd(mcmc.trace('mu')[:], 1-.99)
Out[25]: array([ 2.83464531, 3.04706652])
可能性方法
这也可以借助任何分布的方法nnlf来完成。 在这种情况下,我们需要确定似然性超过1/k的参数值的间隔,其中k为 8(有力证据)或 32(非常有力证据)。
相应间隔的估计然后是优化的简单应用。 我们将此作为练习。
数据挖掘与机器学习
我们将集中讨论三种问题:分类,降维和聚类。 这些问题中的每一个都用于数据挖掘和机器学习中,以得出有关数据的结论。 让我们在不同的部分中解释每个设置。
分类
分类是监督学习的示例。 有一组训练数据,其属性将其分类为几种类别之一。 目的是为新数据找到该属性的值。 例如,通过运行中的数据库,我们可以使用 2013 年以来的所有数据来确定哪些财务投诉为客户带来了积极的解决,哪些财务投诉得到了缓解而没有解决,哪些仍在进行中 。 例如,这将为我们提供很好的见解,例如哪些公司可以更快地积极响应消费者的投诉,某些州解决投诉的可能性较小等。
让我们从查找数据库中观察到的公司回复类型开始:
In [1]: import numpy as np, pandas as pd, matplotlib.pyplot as plt
In [2]: data = pd.read_csv("Consumer_Complaints.csv",
...: low_memory=False, parse_dates=[8,9])
In [3]: print data['Company response'].unique()
['Closed with non-monetary relief' 'In progress' 'Closed with explanation'
'Closed with monetary relief' 'Closed' 'Untimely response'
'Closed without relief' 'Closed with relief']
这是八个不同的类别,也是我们确定未来投诉命运的目标。 让我们通过收集 2013 年提出的所有投诉并创建一个训练数据集,并仅保留我们认为与决策过程相关的列:
- 引发投诉的产品和子产品。
- 消费者与产品有关的问题(但不是子问题)。
- 说明投诉所在的州(而非邮政编码)。
- 投诉方式。
- 提供服务的公司。
该训练数据的大小将指示要用于分类的算法。
提示
在处理数据之前,我们需要对非数字标签进行编码,以便可以使用不同的分类算法对其进行正确处理。 我们使用模块sklearn.preprocessing中的类LabelEncoder进行此操作。
然后,我们将尝试对 2014 年提出的所有投诉进行分类:
In [4]: in_2013 = data['Date received'].map(lambda t: t.year==2013);\
...: in_2014 = data['Date received'].map(lambda t: t.year==2014);\
...: df = data[in_2013 | in_2014]; \
...: df['Year'] = df['Date received'].map(lambda t: t.year); \
...: irrelevant = ['Date received', 'Date sent to company',
...: 'Complaint ID', 'Timely response?',
...: 'Consumer disputed?', 'Sub-issue','ZIP code'];\
...: df.drop(irrelevant, 1, inplace=True); \
...: df = df.dropna()
In [5]: from sklearn.preprocessing import LabelEncoder
In [6]: encoder = {}
In [7]: for column in df.columns:
...: if df[column].dtype != 'int':
...: le = LabelEncoder()
...: le.fit(df[column].unique())
...: df[column] = le.transform(df[column])
...: encoder[column] = le
...:
In [8]: training = df[df.Year==2013]; \
...: target = training['Company response']; \
...: training.drop(['Company response', 'Year'], 1, inplace=True)
In [9]: test = df[df.Year==2014]; \
...: true_result = test['Company response']; \
...: test.drop(['Company response', 'Year'], 1, inplace=True)
In [10]: len(training)
Out[10]: 77100
支持向量分类
此处的培训数据不太大(少于 100,000 的数据被认为是可管理的)。 对于此数量的训练数据,建议我们采用带有线性核的支持向量分类。
该算法的三种形式在模块sklearn.svm(用于支持向量机)中被编码为类:SVC,NuSVC 和具有线性内核的 Linear SVC 的简化版本 SVC,这是我们所需要的:
In [11]: from sklearn.svm import LinearSVC
In [12]: clf = LinearSVC(); \
....: clf.fit(training, target)
Out[12]:
LinearSVC(C=1.0, class_weight=None, dual=True, fit_intercept=True,
intercept_scaling=1, loss='l2', multi_class='ovr', penalty='l2',
random_state=None, tol=0.0001, verbose=0)
我们准备评估此分类器的性能:
In [13]: clf.predict(test)==true_result
Out[13]:
0 False
2 False
3 True
4 True
6 False
7 True
9 True
11 False
12 False
13 False
14 False
15 True
16 True
20 False
21 False
...
101604 True
101607 False
101610 True
101611 True
101613 True
101614 True
101616 True
101617 True
101618 True
101620 True
101621 True
101622 True
101625 True
101626 True
101627 True
Name: Company response, Length: 65282, dtype: bool
In [18]: float(sum(_)) / float(len(_))
Out[18]: 0.7985509022395147
使用这种方法,我们可以正确分类将* 80%的投诉。
提示
在这种方法行不通的极少数情况下,我们始终可以使用经过精心选择的内核求助于普通的SVC或其NuSVC变体。
分类器的功能在于应用程序。 例如,如果我们想通过网络从南卡罗来纳州的美国银行购买常规固定抵押,而我们担心结算过程和成本有问题,该怎么办? 分类者告诉我们有关问题解决的机会?
In [19]: encoder['Product'].transform(['Mortgage'])[0]
Out[19]: 4
In [20]: encoder['Sub-product'].transform(['Conventional fixed mortgage'])[0]
Out[20]: 5
In [21]: encoder['Issue'].transform(['Settlement process and costs'])[0]
Out[21]: 27
In [19]: encoder['State'].transform(['SC'])[0]
Out[19]: 50
In [23]: encoder['Submitted via'].transform(['Web'])[0]
Out[23]: 5
In [24]: encoder['Company'].transform(['Bank of America'])[0]
Out[24]: 247
In [25]: clf.predict([4,5,27,50,5,247])
Out[25]: array([1])
In [26]: encoder['Company response'].inverse_transform(_)[0]
Out[26]: 'Closed with explanation'
满意的外观!
树木
可以创建一个决策树,说明一组有助于分类的规则。 在scikit-learn工具包中,我们为此目的实现了一个类-子模块sklearn.tree中的DecisionTreeClassifier。 让我们来看看它的作用:
In [27]: from sklearn.tree import DecisionTreeClassifier
In [28]: clf = DecisionTreeClassifier().fit(training, target)
In [29]: clf.predict(test) == true_result
Out[29]:
0 True
2 False
3 True
4 True
6 False
7 True
9 True
11 False
12 False
13 False
14 False
15 True
16 True
20 False
21 False
...
101604 True
101607 True
101610 True
101611 True
101613 True
101614 True
101616 True
101617 True
101618 True
101620 True
101621 True
101622 True
101625 True
101626 False
101627 True
Name: Company response, Length: 65282, dtype: bool
In [30]: float(sum(_)) / len(_)
Out[30]: 0.7400661744431849
看起来这个简单的分类器成功地预测了 2014 年约 74%的投诉结果。
提示
可以使用在 http://www.graphviz.org/ 上提供的 Graphviz 可视化软件创建可读的dot文件:
In [31]: from sklearn.tree import export_graphviz
In [32]: export_graphviz(clf, out_file="tree.dot")
在 Graphviz 中打开此文件为我们提供了一组令人印象深刻的规则
以下是树的详细信息(太大而无法容纳在这些页面中!):

我们也有随机森林和极随机树的实现,它们都在子模块sklearn.ensemble中。 相应的类分别称为RandomForestClassifier和ExtraTreesClassifier。
在任何上述情况下,分类器的编码都与 SVC 和基本决策树的情况完全相同。
朴素贝叶斯
使用朴素贝叶斯方法可获得相似的结果。 在模块sklearn.naive_bayes中,我们有此算法的三种实现:
- 高斯朴素贝叶斯类的
GaussianNB类,其中特征的似然性假定为高斯。 - 用于数据的朴素贝叶斯类
BernoulliNB根据多元 Bernoulli 分布进行分布(每个特征均假定为二进制值变量)。 - 用于多重分布数据的类
MultinomialNB。
最*的邻居
为了在这种情况下获得更好的结果,我们采用最*邻居的分类方法。 这与我们在计算几何的设置中用来执行相应的几何查询问题的过程完全相同。 在这种情况下,请注意如何将我们的数据编码为高维欧氏空间中的点,从而可以将这些方法转换为用于此分类的目的。
这种情况下的优势在于,我们不必在计算中使用欧几里得距离。 例如,由于数据无论其数值如何本质上都不同,因此强加汉明度量以计算标签之间的距离是有意义的。 我们对在模块sklearn.neighbors中实现为类KNeighborsClassifier的最*邻居算法进行了概括:
In [33]: from sklearn.neighbors import KNeighborsClassifier
In [34]: clf = KNeighborsClassifier(n_neighbors=8,metric='hamming');\
....: clf.fit(training, target)
Out[34]:
KNeighborsClassifier(algorithm='auto', leaf_size=30,metric='hamming',
n_neighbors=8, p=2, weights='uniform')
In [35]: clf.predict(test)==true_result
Out[35]:
0 True
2 False
3 True
4 True
6 False
7 True
9 True
11 False
12 False
13 False
14 False
15 True
16 True
20 False
21 False
...
101604 True
101607 False
101610 True
101611 True
101613 True
101614 True
101616 True
101617 True
101618 True
101620 True
101621 True
101622 True
101625 True
101626 True
101627 True
Name: Company response, Length: 65282, dtype: bool
In [36]: float(sum(_))/len(_)
Out[36]: 0.791274777120799
成功率超过 79%!
降维
数据通常观察内部结构,但是高维(从某种意义上讲是列数)使其难以提取和选择此内部结构。 通常,可以在低维流形上执行此数据的智能投影,并分析这些投影以搜索特征。 我们将此技术称为降维。
注意
对于以下示例,我们决定删除包含任何NaN的所有日期。 这大大减少了数据量,使后续研究和结果更易于理解。 为了更详尽,更全面的研究,将NaN的所有出现都强制为零-用fillna(0)代替dropna()方法。
让我们通过运行示例观察如何从这些过程中获利。 我们按产品收集所有日常投诉,并分析数据:
In [37]: df = data.groupby(['Date received', 'Product']).size(); \
....: df = df.unstack().dropna()
In [38]: df.head()
Out[38]:
Product Bank account or service Consumer loan Credit card \
Date received
2013-11-06 66 14 41
2013-11-07 44 11 33
2013-11-08 49 11 36
2013-11-09 9 4 20
2013-11-11 15 4 23
Product Credit reporting Debt collection Money transfers Mortgage \
Date received
2013-11-06 62 129 2 153
2013-11-07 43 99 2 128
2013-11-08 44 83 8 113
2013-11-09 19 33 2 23
2013-11-11 32 68 2 46
Product Payday loan Student loan
Date received
2013-11-06 2 14
2013-11-07 8 10
2013-11-08 12 7
2013-11-09 3 4
2013-11-11 2 14
In [39]: df.shape
Out[39]: (233, 9)
我们可以将此数据视为9维空间中的233点。
主成分分析
对于这种少量数据,没有任何其他先验信息,降维的最佳方法之一就是在二维*面上投影。 但是,不仅是任何*面,我们都在寻找一种投影,以确保所投影的数据具有最大可能的方差。 我们使用从代表我们数据的矩阵的特征值和特征向量获得的信息来完成此任务。 该过程称为主成分分析( PCA )。
提示
PCA 被视为统计方法中最有用的技术之一。 要对理论(在线性代数和统计学的两个范围内),编码技术和应用进行令人惊叹的调查,最好的资源是 I.T.出版的《主成分分析》一书的第二版。 Jolliffe 并由 Springer 在其 Springer 系列统计资料中于 2002 年出版。
我们在子模块sklearn.decomposition的scikit-learn工具包中实现了类PCA:
In [40]: from sklearn.decomposition import PCA
In [41]: model = PCA(n_components=2)
In [42]: model.fit(df)
Out[42]: PCA(copy=True, n_components=2, whiten=False)
In [43]: projected_df = model.transform(df)
In [44]: plt.figure(); \
....: plt.scatter(projected_df[:,0], projected_df[:,1]); \
....: plt.title('Principal Component Analysis scatterplot of df'); \
....: plt.show()
观察数据如何由两个非常区分的点集群(其中一个比另一个大得多)以及一些离群值组成。 在下一节中,我们将回到这个问题。

等轴测图
我们不一定需要在超*面上投影。 一个巧妙的技巧是假设数据本身位于非线性子流形上,并获得带有该点的对象的表示。 这使我们可以灵活地搜索投影数据满足相关属性的投影。 例如,如果我们要求投影保持点之间的测地距离(只要可能),就可以实现所谓的等距映射( isomap )。
在 SciPy 堆栈中,我们将此方法实现为子模块sklearn.manifold中的类Isomap:
In [45]: from sklearn.manifold import Isomap
In [46]: model = Isomap().fit(df)
In [47]: isomapped_df = model.transform(df)
In [48]: plt.figure(); \
....: plt.scatter(isomapped_df[:,0], isomapped_df[:,1]); \
....: plt.title('Isometric Map scatterplot of df'); \
....: plt.show()
尽管在视觉上有很大的不同,但此方法还为我们提供了两个非常清晰的群集,其中一个群集比另一个群集大得多。 较小的群集显示为清晰对齐的点序列:

光谱嵌入
另一种可能性是通过将光谱分析应用于亲和度/相似度矩阵来非线性地嵌入数据。 结果的质量与前两个示例相似:
In [49]: from sklearn.manifold import SpectralEmbedding
In [50]: model = SpectralEmbedding().fit(df)
In [51]: embedded_df = model.embedding_
In [52]: plt.figure(); \
....: plt.scatter(embedded_df[:,0], embedded_df[:,1]); \
....: plt.title('Spectral Embedding scatterplot of df'); \
....: plt.show()
在这种情况下,与前面的示例相比,群集的定义更加清晰。

局部线性嵌入
从某种意义上讲,与等轴测图类似,另一种可能的投影试图保留局部邻域内的距离-局部线性嵌入。 我们再次通过子类sklearn.manifold在类LocallyLinearEmbedding中实现:
In [53]: from sklearn.manifold import LocallyLinearEmbedding
In [54]: model = LocallyLinearEmbedding().fit(df)
In [55]: lle_df = model.transform(df)
In [56]: plt.figure(); \
....: plt.scatter(lle_df[:,0], lle_df[:,1]); \
....: plt.title('Locally Linear Embedding scatterplot of df'); \
....: plt.show()
请注意,高度聚集的两个群集和两个异常值。

聚类
从某种意义上说,聚类与分类问题相似,但更为复杂。 当面对数据集时,我们承认具有某种隐藏结构的可能性,这将使我们能够预测未来数据的行为。 通过查找通用模式并在不同集群中收集符合那些模式的数据来执行此结构的搜索。 因此,我们也将此问题称为数据挖掘。
有许多不同的方法可以执行聚类,具体取决于数据量以及关于聚类数量的先验信息。 我们将探索以下设置:
- MeanShift。
- 高斯混合模型。
- K-均值。
- 频谱聚类。
均值漂移
当数据不超过 10,000 点时,我们采用 MeanShift 聚类的技术,并且我们不知道先验需要的聚类数量。 让我们用降维部分中的运行示例进行实验,但是我们将不理会所有投影建议的两个聚类。 我们将让均值漂移聚类为我们做出决定。
我们通过scikit-learn工具包的子模块sklearn.cluster中的类MeanShift在 SciPy 堆栈中实现。 该算法的要素之一是使用径向基函数(在第 1 章,“数值线性代数”中讨论)进行*似,为此我们需要提供适当的带宽。 该算法(如果未提供)将尝试根据数据进行估算。 此过程可能非常缓慢且昂贵,并且通常由我们自己进行估算是一个好主意,因此我们可以控制资源。 我们可以使用同一子模块中的辅助函数estimate_bandwidth来实现。
提示
scikit-learn工具箱中用于聚类算法的类和例程的实现要求将数据作为numpy数组而不是pandas数据帧进行馈送。
我们可以使用dataframe方法.values轻松执行此切换。
In [57]: from sklearn.cluster import MeanShift, estimate_bandwidth
In [58]: bandwidth = estimate_bandwidth(df.values, n_samples=1000)
In [59]: model = MeanShift(bandwidth=bandwidth, bin_seeding=True)
In [60]: model.fit(df.values)
此时,对象model已成功计算了一系列标签并将它们附加到数据中的每个点,因此可以将它们正确聚类。 我们可以允许某些无法分类的点保持未分类的状态-我们通过将可选的布尔值cluster_all设置为False来实现这一点。 默认情况下,该算法将每条数据强制放入一个群集中。
为了达到质量目的,让我们找到标签的数量并将结果可视化为上一部分的投影之一:
In [61]: np.unique(model.labels_) # how many clusters?
Out[61]: array([0, 1, 2])
In [62]: plt.figure(); \
....: plt.scatter(isomapped_df[:,0], isomapped_df[:,1],
....: c=model.labels_, s = 50 + 100*model.labels_); \
....: plt.title('MeanShift clustering of df\n Isometric Mapping \
....: scatterplot\n color/size indicates cluster'); \
....: plt.tight_layout(); \
....: plt.show()
请注意,如何正确计算两个清晰的聚类,并且一个离群值收到了自己的聚类。

让我们找出这些集群的意义。 首先,离群值:
In [63]: df[model.labels_ == 2]
Out[63]:
Product Bank account or service Consumer loan Credit card \
Date received
2014-06-26 117 19 89
Product Credit reporting Debt collection Money transfers Mortgage \
Date received
2014-06-26 85 159 5 420
Product Payday loan Student loan
Date received
2014-06-26 7 12
2014 年 7 月 26 日产生的投诉数量有何不同? 让我们用每个日期簇生成一个图,看看是否可以猜测出它们之间的差异。
In [64]: fig = plt.figure(); \
....: ax1 = fig.add_subplot(211); \
....: ax2 = fig.add_subplot(212); \
....: df[model.labels_==0].plot(ax=ax1); \
....: df[model.labels_==1].plot(ax=ax2); \
....: plt.show()
我们应该得到类似于以下的输出:

从外观上看,似乎集群是由收集大量投诉而不是少量投诉的日期组成的。 不仅如此:仔细检查发现,从第 0 组开始,抵押贷款无疑是投诉的第一原因。 另一方面,对于第 1 组中的日期,对抵押贷款的投诉被降至第二或第三位,始终落后于收债和发薪日贷款:
In [65]: plt.figure(); \
....: df[model.labels_==0].sum(axis=1).plot(label='cluster 0'); \
....: df[model.labels_==1].sum(axis=1).plot(label='cluster 1'); \
....: plt.legend(); \
....: plt.show()
确实是这样:

In [66]: df[model.labels_==0].sum(axis=1).describe()
Out[66]:
count 190.000000
mean 528.605263
std 80.900075
min 337.000000
25% 465.000000
50% 537.000000
75% 585.000000
max 748.000000
dtype: float64
In [67]: df[model.labels_==1].sum(axis=1).describe()
Out[67]:
count 42.000000
mean 156.738095
std 56.182655
min 42.000000
25% 124.250000
50% 140.500000
75% 175.750000
max 335.000000
dtype: float64
In [68]: df[model.labels_==2].sum(axis=1)
Out[68]:
Date received
2014-06-26 913
dtype: float64
请注意,标记为 1 的群集中的投诉量每天不超过 335 件。 从零群集开始的几天里提出的投诉都在 337 到 748 之间。在离群的日期(2014 年 7 月 26 日),有 913 起投诉。
高斯混合模型
高斯混合模型是概率模型,对数据的生成方式和服从的分布进行假设。 这些算法*似于定义所涉及分布的参数。
此方法以其最纯粹的形式实现期望最大化( EM )算法,以拟合模型。 我们使用scikit-learn工具包的子模块sklearn.mixture中的类GMM访问此实现。 不过,此实现要求我们提供所需的群集数量。 与其他方法不同,无论这些人工群集是否具有逻辑意义,它都会尽最大努力将数据分类为所需的多个群集。
为了在不了解所需集群数量的情况下对相对少量的数据执行聚类,我们可以采用高斯混合模型的一种变体,该模型使用变分推断算法来代替。 我们称其为变分高斯混合。 在同一子模块中,我们将此算法实现为类VBGMM。
对于此特定方法,我们确实需要提供我们期望的簇数的上限,但是该算法将为我们计算最佳数目。
例如,在我们的运行示例中(清楚地显示了两个群集),我们将上限设置为 30,并观察VBGMM算法的行为:
In [69]: from sklearn.mixture import VBGMM
In [70]: model = VBGMM(n_components=30).fit(df)
In [71]: labels = model.predict(df)
In [72]: len(np.unique(labels)) # how many clusters?
Out[72]: 2
只有两个集群!
In [73]: a, b = np.unique(labels)
In [74]: sizes = 50 + 100 * (labels - a) / float(b-a)
In [75]: plt.figure(); \
....: plt.scatter(embedded_df[:,0], embedded_df[:,1],
....: c=labels, s=sizes); \
....: plt.title('VBGMM clustering of df\n Spectral Embedding \
....: scatterplot\n color/size indicates cluster'); \
....: plt.tight_layout(); \
....: plt.show()
我们应该得到类似于以下的输出:

me
如果我们以前知道所需的聚类数,而无论数据量如何,那么劳埃德算法(最好称为 K-means 方法)就是一个很好的聚类算法。
在模块scipy.cluster.vq中,我们提供了一套有效的 k 均值聚类例程。 scikit-learn工具包的子模块sklearn.cluster中的类KMeans实现了并行算法。 例如,如果我们需要使用计算机的所有 CPU 在数据上将分区划分为四个集群,则可以从工具箱中发出以下代码:
In [76]: from sklearn.cluster import KMeans
In [77]: model = KMeans(n_clusters=4, n_jobs=-1).fit(df)
In [78]: plt.figure(); \
....: plt.scatter(isomapped_df[:,0], isomapped_df[:,1],
....: c=model.labels_, s=10 + 100*model.labels_); \
....: plt.title('KMeans clustering of df\n Isometric Mapping \
....: scatterplot\n color/size indicates cluster'); \
....: plt.tight_layout(); \
....: plt.show()
我们应该得到类似于以下的输出:

请注意,无论产品如何,该人工聚类仍然如何根据收到的投诉数量对不同的日期进行分类:
In [79]: plt.figure()
In [80]: for label in np.unique(model.labels_):
....: if sum(model.labels_==label) > 1:
....: object = df[model.labels_==label].sum(axis = 1)
....: object.plot(label=label)
....:
In [81]: plt.legend(); \
....: plt.show()
我们应该得到类似于以下的输出:

提示
就像我们之前的聚类分析一样,此图中未包含的聚类是 2014 年 7 月 26 日这一天,当时收到了*千个投诉。
在海量数据(超过 10,000 点)的情况下,我们经常使用 K 均值的变体,该变体在不同的迭代中对数据的随机采样子集运行,以减少计算时间。 此方法称为小批量 Kmeans ,并且已在同一子模块中实现为类MiniBatchKMeans。 与使用纯 K 均值相比,聚类的质量稍差一些,但是过程明显更快。
光谱聚类
通过在 K 均值之前对数据进行低维频谱嵌入(具有不同的指标),当任何先前方法未能以有意义的方式对数据进行分类时,我们通常能够解决聚类问题。 在scikit-learn工具包中,我们有一个基于代数多重网格求解器的非常聪明的实现,作为子模块sklearn.cluster中的类SpectralClustering。
提示
要处理代数多重网格求解器,强烈建议安装软件包pyamg。 该软件包由伊利诺伊大学香槟分校伊利诺伊大学计算机系的 Nathan Bell,Luke Olson 和 Jacob Schroder 开发。 这不是绝对必要的,但是这样做将极大地加快我们的计算速度。 可以从 http://pyamg.org/ 以多种格式下载该软件包,也可以从控制台使用pip,easy_install或conda命令照常安装。
摘要
在本章中,我们探索了 SciPy 堆栈中的高级技术,以执行推断统计,数据挖掘和机器学习。 在下一章中,我们将完全改变齿轮以掌握数字图像处理。
九、数字图像处理
“数字图像处理”是一个非常广阔的领域,它通过将图像表示为数学对象来处理图像。 根据目标,我们有四个子字段:
-
Image acquisition: The concern here is the effective representation of an object as an image. Clear examples are the digitalization of a photograph (that could be coded as a set of numerical arrays), or super-imposed information of the highest daily temperatures on a map (that could be coded as a discretization of a multivariate function). The processes of acquisition differ depending on what needs to be measured and the hardware that performs the measures. This topic is beyond the scope of this book but, if interested, some previous background can be obtained by studying the Python interface to OpenCV and any of the background libraries, such as Python Imaging Library (PIL) and the friendly PIL fork Pillow.
提示
可以通过 http://effbot.org/imagingbook/pil-index.htm 上的 http://effbot.org/ 页面访问有关 PIL 的出色文档。 立即安装 SciPy 堆栈会在我们的系统中放置最新版本的 PIL 的副本。 如果需要,可以从 http://pythonware.com/products/pil/ 单独下载该库。 有关 Pillow 的信息,请参考 http://pillow.readthedocs.org/ 。
可以在 http://opencv.org/ 中找到有关 OpenCV 的良好信息。 为了进一步了解 Python 的接口,我发现 http://docs.opencv.org/3.0-beta/doc/py_tutorials/py_tutorials.html 上的教程非常有用。
请注意,为 Python 安装 OpenCV 并不容易。 我的建议是从 Anaconda 或任何其他科学 Python 发行版执行此类安装。
-
图像压缩:这是这些子字段中技术最先进的,并且主要需要 NumPy,SciPy 和一些其他软件包中的高级库。 目标是使用尽可能少的数据来表示图像,从而保留大部分(理想情况下)相关信息。
-
Image editing: This, together with the following image analysis, is what we refer to as Image processing. Examples of the goals of image editing range from the restoration of damaged photographs, to the deblurring of a video sequence, or the removal of an object in an image, so that the removed area gets inpainted with coherent information. To deal with these operations, in the SciPy stack, we have the library
scipy.ndimage, and the image processing toolkitscikit-image.提示
可以在 http://docs.scipy.org/doc/scipy/reference/tutorial/ndimage.html 上找到有关多维图像处理库
scipy.ndimage的大量参考资料和文档。 过滤器的启发性介绍。要探索图像处理工具包
scikit-image,一个很好的初始资源是 http://scikit-image.org/docs/stable/ 官方页面上的文档。 这包括使用 NumPy 拍摄图像的速成班。 -
图像分析:这是一个有趣的领域,我们的目标是从表示为图像的对象中获取不同的信息。 想一想可以在一大群人的视频渲染中跟踪一个人的脸,或者在催化剂的显微照片上计算金原子数量的代码。 对于这些任务,我们通常将前两个库中的函数与上一章中讨论过的有用的工具包
scikit-learn混合使用。
在我们的博览会中,我们将从一小部分开始,介绍如何在 SciPy 堆栈中表示数字图像。 我们将继续第二部分,介绍图像基本操作的性质。 其余各节将按顺序介绍压缩,编辑和分析技术。
我们介绍的大多数操作都以示例的可视化结束。 相应的代码通常是matplotlib命令的简单应用。 这些代码通常不包括在内,作为练习留给读者。 仅当引入特定的复杂布局或新颖的想法时,我们才会在演示文稿中包含这些代码。
数码影像
字典将像素(像素的缩写)定义为显示屏上照明的微小区域,从中构成图像是其中的一个。 因此,我们将数字图像视为一组像素,每个像素均由其位置(与选择的坐标类型无关)和该位置上相应图像的光强度来定义。
根据我们测量强度的方式,数字图像属于以下三种可能的类型之一:
- 二元
- 灰阶
- 颜色(有或没有 Alpha 通道)
二元
在二进制图像中,只有两种可能的强度-亮或暗。 传统上,此类图像最好实现为简单的二维布尔数组。 真表示亮点,而假表示暗点。
例如,要创建一个大小为 128 x 128 的二进制映像,并以半径为 6 的单个磁盘居中于位置(30,100),我们可以发出以下命令:
In [1]: import numpy as np, matplotlib.pyplot as plt
In [2]: disk = lambda x,y: (x-30)**2 + (y-100)**2 <= 36
In [3]: image = np.fromfunction(disk, (128, 128))
In [4]: image.dtype
Out[4]: dtype('bool')

提示
在二进制图像上生成几何形状的另一种方法是模块skimage.draw或skimage.morphology中的一组实用程序。 例如,以前的代码可能生成如下:
>>> from skimage.draw import circle
>>> image = np.zeros((128, 128)).astype('bool')
>>> image[circle(30, 100, 6)] = True
模块skimage.draw具有创建其他二维几何形状的例程:
- 行:
line。 对于灰度图像,还存在线条的抗锯齿版本:line_aa。 - 圆圈:
circle,circle_perimeter。 对于灰度图像,还有一个圆周抗锯齿版本:circle_perimeter_aa。 - 椭圆:
ellipse,ellipse_perimeter。 - 多边形:
polygon。
灰阶
灰度图像是代表黑白照片的传统方法。 在这些图像中,光的强度表示为不同的灰度等级。 白色表示最亮,黑色表示没有光。 不同刻度的数量是预先确定的,通常是二进位数(例如,我们可以选择少至 16 个刻度,或多达 256 个)。 在任何情况下,最高值始终保留为最亮的颜色(白色),最低值始终保留为最暗的颜色(黑色)。 一个简单的二维数组是存储此信息的好方法。
scipy.misc库具有符合该类别的测试图像。 在工具包skimage中,我们还有一些具有相同特征的测试图像:
In [6]: from scipy.misc import lena; \
...: from skimage.data import coins
In [7]: lena().shape
Out[7]: (512, 512)
In [8]: lena()
Out[8]:
array([[162, 162, 162, ..., 170, 155, 128],
[162, 162, 162, ..., 170, 155, 128],
[162, 162, 162, ..., 170, 155, 128],
...,
[ 43, 43, 50, ..., 104, 100, 98],
[ 44, 44, 55, ..., 104, 105, 108],
[ 44, 44, 55, ..., 104, 105, 108]])
In [9]: coins().shape
Out[9]: (303, 384)
In [10]: coins()
Out[10]:
array([[ 47, 123, 133, ..., 14, 3, 12],
[ 93, 144, 145, ..., 12, 7, 7],
[126, 147, 143, ..., 2, 13, 3],
...,
[ 81, 79, 74, ..., 6, 4, 7],
[ 88, 82, 74, ..., 5, 7, 8],
[ 91, 79, 68, ..., 4, 10, 7]], dtype=uint8)

提示
在左侧,我们可以看到 Lena,这是从 1972 年 11 月的《花花公子》杂志上扫描出来的标准(有争议的)测试图像。 在右侧,我们可以看到庞贝古城的希腊硬币。 该图像已从布鲁克林博物馆收藏中下载。
颜色
在彩色图像中,我们有许多不同的方法来存储基础信息。 最常见的方法是 RGB 颜色空间,它也提供了用于创建算法的最简单的计算结构。 在这种方法中,图像表示至少包含三层。 对于每个像素,我们评估在相应位置获得所需颜色和强度所需的红色,绿色和蓝色量的组合信息。 第一层指示底层红色的强度。 第二层和第三层分别指示绿色和蓝色的强度:
In [12]: from skimage.data import coffee
In [13]: coffee().shape
Out[13]: (400, 600, 3)
In [14]: coffee()
Out[14]:
array([[[ 21, 13, 8],
[ 21, 13, 9],
[ 20, 11, 8],
...,
[228, 182, 138],
[231, 185, 142],
[228, 184, 140]],
...,
[[197, 141, 100],
[195, 137, 99],
[193, 138, 98],
...,
[158, 73, 38],
[144, 64, 30],
[143, 60, 29]]], dtype=uint8)

这张照片由 Rachel Michetti 拍摄,由 Pikolo Espresso Bar 提供。
为了收集与每个图层对应的数据,我们执行简单的切片操作:
In [15]: plt.figure(); \
....: plt.subplot(131); \
....: plt.imshow(coffee()[:,:,0], cmap=plt.cm.Reds); \
....: plt.subplot(132); \
....: plt.imshow(coffee()[:,:,1], cmap=plt.cm.Greens); \
....: plt.subplot(133); \
....: plt.imshow(coffee()[:,:,2], cmap=plt.cm.Blues); \
....: plt.show()

本章中介绍的 SciPy 堆栈库中的所有功能均假定该方案中表示了任何彩色图像。 还有其他配色方案,旨在解决其他基本问题和图像属性。 在工具包scikit-image中,可以在子模块skimage.color中使用函数convert-colorspace来在大多数常见色彩空间之间进行转换。 例如,考虑色相饱和度值( HSV )颜色空间。 这是来自 RGB 颜色空间的点的圆柱坐标表示,其中围绕中心垂直轴的角度对应于色调( H ),而距轴的距离对应于[ 饱和度( S )。 高度对应于第三个值( V ),即相对于饱和度的系统感知亮度(表示基础颜色组合的亮度)的表示形式:
In [16]: from skimage.color import convert_colorspace
In [17]: convert_colorspace(coffee(), 'RGB', 'HSV')
Out[17]:
array([[[ 0.06410256, 0.61904762, 0.08235294],
[ 0.05555556, 0.57142857, 0.08235294],
[ 0.04166667, 0.6 , 0.07843137],
...,
[ 0.08148148, 0.39473684, 0.89411765],
[ 0.08052434, 0.38528139, 0.90588235],
[ 0.08333333, 0.38596491, 0.89411765]],
...,
[[ 0.07044674, 0.49238579, 0.77254902],
[ 0.06597222, 0.49230769, 0.76470588],
[ 0.07017544, 0.49222798, 0.75686275],
...,
[ 0.04861111, 0.75949367, 0.61960784],
[ 0.0497076 , 0.79166667, 0.56470588],
[ 0.04532164, 0.7972028 , 0.56078431]]])
提示
其他可用的色彩空间包括测量三色刺激值的 CIE XYZ 方法,或 CIE-LUB 和 CIE-LAB 色彩空间。 学习如何在 SciPy 堆栈环境中访问和使用它们的最佳资源是模块skimage.color的文档,位于其官方文档的页面上,网址为,网址为 http://scikit-image.org/docs/stable/。 api / skimage.color.html 。
通过添加具有适当权重的三层,还可以生成 RGB 颜色空间中提供的任何彩色图像的灰度版本。 在skimage.color模块中,例如,我们具有使用公式output = 0.2125*red + 0.7154*green + 0.0721*blue的函数rgb2gray或rgb2grey。
Alpha 频道
在灰度或彩色图像中,有时我们会指示一个额外的图层,该图层为我们提供有关每个像素的不透明度的信息。 这称为 alpha 通道。 传统上,我们将图像的此属性合并为 RGB 的附加层,即 RGBA 色彩空间。 在这种情况下,此方案表示的图像具有四层而不是三层:
In [18]: from skimage.data import horse
In [19]: horse().shape
Out[19]: (328, 400, 4)
In [20]: horse()
Out[20]:
array([[[255, 255, 255, 110],
[255, 255, 255, 217],
[255, 255, 255, 255],
...,
[255, 255, 255, 255],
[255, 255, 255, 217],
[255, 255, 255, 110]],
...,
[[255, 255, 255, 110],
[255, 255, 255, 217],
[255, 255, 255, 255],
...,
[255, 255, 255, 255],
[255, 255, 255, 217],
[255, 255, 255, 110]]], dtype=uint8)
对数字图像的高级操作
在解决图像处理和压缩的挑战之前,值得一提的是检查我们对图像执行的基本操作的顺序。 这些运算是我们要探索的算法的基础。 他们自己精美地说明了数字图像处理的基本原理。 图像表示为数学对象,因此,可以将对相应对象的基本操作转换为对相应图像的物理操作或查询。
物体测量
在二进制图像的设置中,我们可以将图像视为*面的空白区域(背景为黑色)上的一组对象或斑点(白色)。 然后可以对所表示的每个对象执行不同的措施:
In [1]: import numpy as np, matplotlib.pyplot as plt; \
...: from skimage.data import hubble_deep_field
In [2]: image = (hubble_deep_field()[:,:,0] > 120)

提示
哈勃极致深场:左图显示了宇宙的最远视图。 哈勃望远镜为 NASA 捕获了它,并将其上传到 http://hubblesite.org/ 。 可以在公共领域免费使用。
右边的图像(二进制图像)作为True值的子集,收集原始图像中代表的天体的选择。 我们通过简单的阈值操作获得了该二进制图像,我们在其中要求图片的红色强度大于 120 的那些像素。
从该二值图像中,我们可以轻松地标记和计数所选的天体,并计算其某些几何特性。 我们通过使用库scipy.ndimage中的label函数来实现:
In [4]: from scipy.ndimage import label
In [5]: labels, num_features = label(image); \
...: print "Image contains {} objects".format(num_features)
Image contains 727 objects.
例如,使用功能center_of_mass计算每个物体的重心:
In [6]: from scipy.ndimage import center_of_mass
In [7]: for k in range(1,11):
...: location = str(center_of_mass(image, labels, k))
...: print "Object {} center of mass at {}".format(k,location)
...:
Object 1 center of mass at (0.0, 875.5)
Object 2 center of mass at (4.7142857142857144, 64.857142857142861)
Object 3 center of mass at (3.3999999999999999, 152.19999999999999)
Object 4 center of mass at (6.0454545454545459, 206.13636363636363)
Object 5 center of mass at (5.0, 489.5)
Object 6 center of mass at (6.5, 858.0)
Object 7 center of mass at (6.0, 586.5)
Object 8 center of mass at (7.1111111111111107, 610.66666666666663)
Object 9 center of mass at (10.880000000000001, 297.45999999999998)
Object 10 center of mass at (12.800000000000001, 132.40000000000001)
数学形态
同样,在二进制图像的设置中,我们还有另一套有趣的操作,数学形态学。 基本的形态学操作包括使用常见的结构元素探测斑点的形状。 例如,考虑使用小正方形作为结构元素的形状腐蚀和膨胀的基本操作。
物体的腐蚀是当该物体在原始物体内部移动时,可以通过结构元素中心到达的该物体的点集。 另一方面,对象的膨胀是当结构的中心在原始对象内移动时由结构元素覆盖的一组点。 这两个操作的序列的组合导致图像编辑中功能更强大的算法。
另外,让我们观察两个更高级的形态学操作:形状骨架的计算以及形状中轴的位置(其距离变换的脊线)。 这些操作也是图像分析中有趣的过程的种子:
In [8]: from scipy.ndimage.morphology import binary_erosion; \
...: from scipy.ndimage.morphology import binary_dilation; \
...: from skimage.morphology import skeletonize, medial_axis; \
...: from skimage.data import horse
In [9]: image = horse()[:,:,0]==0
In [10]: # Morphology via scipy.ndimage.morphology ; \
....: structuring_element = np.ones((10,10)); \
....: erosion = binary_erosion(image, structuring_element); \
....: dilation = binary_dilation(image, structuring_element)
In [11]: # Morphology via skimage.morphology ; \
....: skeleton = skeletonize(image); \
....: md_axis = medial_axis(image)
In [12]: plt.figure(); \
....: plt.subplot2grid((2,4), (0,0), colspan=2, rowspan=2); \
....: plt.imshow(image); \
....: plt.gray(); \
....: plt.title('Original Image'); \
....: plt.subplot2grid((2,4), (0,2)); \
....: plt.imshow(erosion); \
....: plt.title('Erosion'); \
....: plt.subplot2grid((2,4), (0,3)); \
....: plt.imshow(dilation); \
....: plt.title('Dilation'); \
....: plt.subplot2grid((2,4), (1,2)); \
....: plt.imshow(skeleton); \
....: plt.title('Skeleton'); \
....: plt.subplot2grid((2,4), (1,3)); \
....: plt.imshow(md_axis); \
....: plt.title('Medial Axis'); \
....: plt.show()

提示
马的黑白剪影,由 Andreas Preuss 绘制并上传到 https://openclipart.org/ 的公共领域。
*滑过滤器
我们可以将图像视为多元函数。 在这种情况下,有一些操作可以计算出具有某些良好属性的原始值的*似值。 一个典型的例子是创建图像的*滑版本。 这些是算法的基石,其中存在噪声或不必要的复杂纹理可能会导致令人困惑的结果。
以高斯滤波器为例,一个函数与均值mu=0和用户定义的标准deviation sigma的高斯核的卷积:
In [13]: from scipy.ndimage import gaussian_filter; \
....: from skimage.color import rgb2gray; \
....: from skimage.data import coffee
In [14]: image = coffee()
In [15]: smooth_image = gaussian_filter(rgb2gray(image), sigma=2.5)

请注意,右侧的图像(原始图像的*滑版本)似乎模糊。 因此,了解*滑机制也是图像恢复算法的良好基础。
多元演算
现在通过将图像视为足够*滑的强度函数(使用或不使用*滑滤波器),就多元演算技术而言,可以进行许多操作。 例如,Prewitt 和 Sobel 运算符计算*似于所述函数的梯度范数。 每个位置上的相应值评估具有边缘的可能性,因此可用于构造可靠的特征检测算法:
In [17]: from scipy.ndimage import prewitt
In [18]: gradient_approx = prewitt(smooth_image)

由于原始图像的特性及其*滑版本,在这种情况下,渐变幅度的绝对值(右)的范围从 0(黑色)到 0.62255043871863613(白色)。 因此,较亮的区域表示可能的边缘的位置,而较暗的区域表示较*坦的区域的位置。
二阶导数的和(拉普拉斯算子)也用于特征检测或运动估计的算法中:
In [20]: from scipy.ndimage import laplace
In [21]: laplace_approx = laplace(smooth_image)

在这种情况下,拉普拉斯信息与梯度的组合会为局部极值的位置和所表示对象的几何形状提供线索。
Hessian 矩阵(标量值函数的二阶偏导数)用于描述图像的局部曲率。 它是斑点检测过程中的有用组件。 我们在模块skimage.feature中将这个运算符实现为例程hessian_matrix。 为了*似于黑森州的行列式,我们在同一模块中有例程hessian_matrix_det。
统计过滤器
将图像视为多维信号,我们可以进行许多具有统计性质的滤波操作。
例如,可以分别使用scipy.ndimage中的函数maximum_filter,minimum_filter,median_filter或percentile_filter计算最大,最小,中值或百分位滤波器。 这些滤波器针对图像上的每个像素和给定的足迹分别计算以像素为中心的足迹上的图像的最大,最小,中位数或请求的百分位。
在以下示例中,我们使用 10 x 10 正方形作为占位面积来计算第 80 个百分位数:
In [23]: from scipy.ndimage import percentile_filter
In [24]: prctl_image = percentile_filter(image[:,:,0],
....: percentile=-20, size=10)

可在子模块skimage.filters.rank中找到此类别中更多相关的过滤器。
傅立叶分析
通过再次将图像视为多元函数,我们可以对其进行傅立叶分析。 傅里叶变换和离散余弦变换的应用主要用于过滤,从图像中提取信息以及压缩:
In [25]: from scipy.fftpack import fft2, ifft2, fftshift; \
....: from skimage.data import text
In [26]: image = text()
In [27]: frequency = fftshift(fft2(image))
函数的频率通常是复数值函数。 为了使其可视化,我们将介绍每个输出值的模块和角度。 为了更好地解释,我们通常通过对频率的模块应用对数校正来在视觉上增强结果:
In [28]: plt.figure(); \
....: ax1 = plt.subplot2grid((2,2), (0,0), colspan=2); \
....: plt.imshow(image)
Out[28]: <matplotlib.image.AxesImage at 0x11deb1650>
In [29]: module = np.absolute(frequency); \
....: angles = np.angle(frequency)
In [30]: from skimage.exposure import adjust_log
In [31]: ax2 = plt.subplot2grid((2,2), (1,0)); \
....: plt.imshow(adjust_log(module)); \
....: ax3 = plt.subplot2grid((2,2), (1,1)); \
....: plt.imshow(angles); \
....: plt.show()
注意
文本是从 Wikipedia 下载并发布到公共领域的图像。 可以在 https://en.wikipedia.org/wiki/File:Corner.png 中找到。

请注意,如果我们忽略了图像频率中的部分信息(例如,大约 25%的信息),然后执行反演,将会发生什么:
In [32]: frequency.shape
Out[32]: (172, 448)
In [33]: smaller_frequency = frequency[:,448/2-172/2:448/2+172/2]
In [34]: new_image = ifft2(smaller_frequency); \
....: new_image = np.absolute(new_image)

尽管我们错过了原始频率的四分之一,但反演为我们提供了一个图像,该图像具有与原始频率完全相同的信息。 我们忽略了那部分较低频率实际上实际上失去了什么? 这个问题的答案导致了有趣的重建,压缩和分析算法。
小波分解
我们可以使用 Tariq Rashid 编写的软件包PyWavelets执行小波分解。
提示
可以从 https://pypi.python.org/pypi/PyWavelets 下载。 可以按照这些页面上的说明进行后安装。 对于某些体系结构,安装可能很棘手。 在这种情况下,我们建议您从科学的 Python 发行版(例如 Anaconda)中进行工作。 例如,我们可以使用binstar/conda命令搜索软件包:
% binstar search -t conda pywavelets
% conda install -c conda.binstar.org/dgursoy pywavelets
请注意,此库中实现了许多不同的小波系列:
In [36]: import pywt
In [37]: pywt.families()
Out[37]: ['haar', 'db', 'sym', 'coif', 'bior', 'rbio', 'dmey']
In [38]: print pywt.wavelist()
['bior1.1', 'bior1.3', 'bior1.5', 'bior2.2', 'bior2.4', 'bior2.6',
'bior2.8', 'bior3.1', 'bior3.3', 'bior3.5', 'bior3.7', 'bior3.9',
'bior4.4', 'bior5.5', 'bior6.8', 'coif1', 'coif2', 'coif3', 'coif4',
'coif5', 'db1', 'db2', 'db3', 'db4', 'db5', 'db6', 'db7', 'db8',
'db9', 'db10', 'db11', 'db12', 'db13', 'db14', 'db15', 'db16',
'db17', 'db18', 'db19', 'db20', 'dmey', 'haar', 'rbio1.1','rbio1.3',
'rbio1.5', 'rbio2.2', 'rbio2.4', 'rbio2.6', 'rbio2.8', 'rbio3.1',
'rbio3.3', 'rbio3.5', 'rbio3.7', 'rbio3.9', 'rbio4.4', 'rbio5.5',
'rbio6.8', 'sym2', 'sym3', 'sym4', 'sym5', 'sym6', 'sym7', 'sym8',
'sym9', 'sym10', 'sym11', 'sym12', 'sym13', 'sym14', 'sym15',
'sym16', 'sym17', 'sym18', 'sym19', 'sym20']
让我们看看如何使用haar小波计算图像skimage.data.camera的表示形式。 由于原始图像的边长为512=2^9,因此在小波系数的计算中我们将需要九个级别:
In [39]: from skimage.data import camera
In [40]: levels = int(np.floor(np.log2(camera().shape).max())); \
....: print "We need {} levels".format(levels)
We need 9 levels
In [41]: wavelet = pywt.Wavelet('haar')
In [42]: wavelet_coeffs = pywt.wavedec2(camera(), wavelet,
....: level=levels)
对象wavelet_coeffs是具有十个条目的元组,第一个是最高级别 0 的*似值。这始终是一个单一系数。 wavelet_coeffs中的第二个条目是一个三元组,包含级别 1 的三个不同的细节(水*,垂直和对角线)。每个连续的条目是另一个 3 元组,其中包含较高级别的三个不同的细节(n = 2, 3, 4, 5, 6, 7, 8, 9) 。
注意每个级别的系数数:
In [43]: for index, level in enumerate(wavelet_coeffs):
....: if index > 0:
....: value = level[0].size + level[1].size + level[2].size
....: print "Level {}: {}".format(index, value)
....: else:
....: print "Level 0: 1"
....:
Level 0: 1
Level 1: 3
Level 2: 12
Level 3: 48
Level 4: 192
Level 5: 768
Level 6: 3072
Level 7: 12288
Level 8: 49152
Level 9: 196608
图像压缩
压缩的目的是通过比仅将每个像素存储在阵列中所需的信息单元(例如字节)少的方法来表示图像。
例如,回想一下我们在第一部分中构建的二进制图像; 那是由 16,384 位(True / False)表示的 128 x 128 图像,其中除 113 位之外的所有位都是False。 当然,必须有一种更有效的方法来以少于 16384 位的方式存储此信息。 我们只需提供画布的大小(两个字节),磁盘中心的位置(再增加两个字节)以及其半径值(另一个字节)就可以做到这一点。 现在,我们有了一个仅使用 40 位的新表示形式(假设每个字节由 8 位组成)。 我们将这种精确表示称为无损压缩。
压缩图像的另一种可能方式是例如将彩色图像转换为其黑白表示的过程。 我们对图像skimage.data.coffee执行了此操作,将大小为 3 x 400 x 600(720,000 字节)的对象变成大小为 400 x 600(240,000 字节)的对象。 尽管在此过程中,我们失去了查看其颜色的能力。 这种操作适当地称为有损压缩。
在接下来的页面中,我们将从数学的角度探讨图像压缩的几种设置。 我们还将开发高效的代码,以从 SciPy 堆栈内部执行这些操作。 我们与创建用于读取或保存这些压缩图像的代码无关。 为此,我们在 Python Imaging Library 中已经有了可靠的实用程序,这些实用程序也已导入到模块scipy.misc,scipy.ndimage和工具包scikit-image中的不同函数中。 如果我们希望将代表黑白摄影的numpy数组 A 压缩并存储为不同的文件类型,我们可以简单地按照以下方式发布内容:
In [1]: import numpy as np; \
...: from scipy.misc import lena, imsave
In [2]: A = lena()
In [3]: imsave("my_image.png", A); \
...: imsave("my_image.tiff", A); \
...: imsave("my_image.pcx", A); \
...: imsave("my_image.jpg", A); \
...: imsave("my_image.gif", A)
快速查看我们正在处理的文件夹的内容,显示创建的文件的大小。 例如,在* NIX 系统下,我们可以发出以下命令:
% ls -nh my_image.*
-rw-r--r-- 1 501 20 257K May 29 08:16 my_image.bmp
-rw-r--r-- 1 501 20 35K May 29 08:16 my_image.jpg
-rw-r--r-- 1 501 20 273K May 29 08:15 my_image.pcx
-rw-r--r-- 1 501 20 256K May 29 08:16 my_image.tiff
注意结果文件的大小不同。 无损格式 PCX,BMP 和 TIFF 提供类似的压缩率(分别为 273K,257K 和 256K)。 另一方面,JPEG 有损格式提供了明显的改进(35 K)。
无损压缩
用于图像处理的一些最常见的无损压缩方案如下:
- 游程长度编码:当原始图像可视为基于调色板的位图(例如,卡通或计算机图标)时,此方法用于 PCX,BMP,TGA 和 TIFF 文件类型。
- Lempel-Ziv-Welch(LZW):默认情况下,它以 GIF 图像格式使用。
- 通缩:这非常强大且可靠。 这是用于 PNG 图像文件的方法。 它也是用于创建 ZIP 文件的压缩方法。
- 链码:这是编码二进制图像的首选方法,尤其是当这些图像包含少量大斑点时。
例如,让我们研究一个合适的示例中游程长度编码是如何工作的。 考虑棋盘图像skimage.data.checkerboard。 我们以 200 x 200 的整数值数组形式接收它,因此,它需要 40,000 字节的存储空间。 注意,它可以被视为只有两种颜色的基于调色板的位图。 我们首先将每个零值转换为B,然后将每个 255 转换为W:
In [5]: from skimage.data import checkerboard
In [6]: def color(value):
...: if value==0: return 'B'
...: else: return 'W'
...:
In [7]: image = np.vectorize(color)(checkerboard()); \
...: print image
[['W' 'W' 'W' ..., 'B' 'B' 'B']
['W' 'W' 'W' ..., 'B' 'B' 'B']
['W' 'W' 'W' ..., 'B' 'B' 'B']
...,
['B' 'B' 'B' ..., 'W' 'W' 'W']
['B' 'B' 'B' ..., 'W' 'W' 'W']
['B' 'B' 'B' ..., 'W' 'W' 'W']]
接下来,我们创建一个对字符串列表和字符串进行编码的函数,而是生成一个由“单字符加计数”形式的模式组成的字符串:
In [7]: from itertools import groupby
In [8]: def runlength(string):
...: groups = [k + str(sum(1 for _ in g)) for k,g in
...: groupby(string)]
...: return ''.join(groups)
...:
请注意,当我们将图像重写为包含其颜色的扁*字符串并以这种方式对其进行编码时,会发生什么:
In [9]: coded_image = runlength(image.flatten().tolist())
In [10]: print coded_image
W26B23W27B23W27B23W27B24W26B23W27B23W27B23W27B24W26B23W27B23W27B23W27
B24W26B23W27B23W27B23W27B24W26B23W27B23W27B23W27B24W26B23W27B23W27B23
W27B24W26B23W27B23W27B23W27B24W26B23W27B23W27B23W27B24W26B23W27B23W27
B23W27B24W26B23W27B23W27B23W27B24W26B23W27B23W27B23W27B24W26B23W27B23
W27B23W27B24W26B23W27B23W27B23W27B24W26B23W27B23W27B23W27B24W26B23W27
B23W27B23W27B24W26B23W27B23W27B23W27B24W26B23W27B23W27B23W27B24W26B23
W27B23W27B23W27B24W26B23W27B23W27B23W27B24W26B23W27B23W27B23W27B24W26
B23W27B23W27B23W27B24W26B23W27B23W27B23W27B24W26B
...
26B24W27B23W27B23W27B23W26B24W27B23W27B23W27B23W26B24W27B23W27B23W27B
23W26B24W27B23W27B23W27B23W26B24W27B23W27B23W27B23W26B24W27B23W27B23W
27B23W26B24W27B23W27B23W27B23W26B24W27B23W27B23W27B23W26B24W27B23W27B
23W27B23W26B24W27B23W27B23W27B23W26B24W27B23W27B23W27B23W26B24W27B23W
27B23W27B23W26B24W27B23W27B23W27B23W26B24W27B23W27B23W27B23W26B24W27B
23W27B23W27B23W26B24W27B23W27B23W27B23W26B24W27B23W27B23W27B23W26B24W
27B23W27B23W27B23W26B24W27B23W27B23W27B23W26B24W27B23W27B23W27B23W26
In [11]: len(coded_image)
Out[11]: 4474
我们将其大小减小到只有 4,474 字节。 现在,您如何将这些信息解码回图像? 假设为您提供了该字符串,图像大小的附加信息(200 x 200)和调色板信息(黑色为B,白色为W)。
另一个不错的练习是找到其他提到的无损压缩方法的描述,并为其对应的编码器和解码器编写 Python 代码。
有损压缩
在许多可能的有损压缩方案中,我们将集中于变换编码的方法。 例如,文件类型 JPEG 基于离散余弦变换。
在任何这些情况下,过程都是相似的。 我们假设图像是一个函数。 它的可视化可视为其图形的表示,因此这是一个空间操作。 相反,我们可以计算图像的变换(例如,傅立叶,离散余弦或小波)。 现在,图像由值的集合表示:相应变换中函数的系数。 现在,当我们忽略大量这些系数并使用相应的逆变换重构函数时,就会发生压缩。
在处理上一节中的傅立叶分析技术时,我们已经观察到在忽略图像较低频率 25%之后重建图像的行为。 这不是忽略系数的唯一方法。 例如,我们可以改为收集具有足够大的绝对值的系数。 让我们检查一下这次使用离散余弦变换对同一图像执行该操作的结果:
In [12]: from skimage.data import text; \
....: from scipy.fftpack import dct, idct
In [13]: image = text().astype('float')
In [14]: image_DCT = dct(image)
让我们忽略绝对值小于或等于 1000 的值。 请注意,此类系数的数量超过 256,317(几乎是原始数据的 98%):
In [15]: mask = np.absolute(image_DCT)>1000
In [16]: compressed = idct(image_DCT * mask)

尽管丢掉了大多数系数,但是重建还是非常忠实的。 有明显的文物,但这些文物并不太分散注意力。
我们可以使用小波变换执行类似的操作。 在此设置下执行压缩的一种简单方法是忽略整个系数级别,然后进行重构。 在前面部分中的示例(具有九个系数级别的图像skimage.data.camera的 Haar 小波表示)中,如果我们消除了后两个层次,则将丢弃 245760 个系数(几乎占原始信息的 94% )。 观察重建质量:
In [18]: import pywt; \
....: from skimage.data import camera
In [19]: levels = int(np.floor(np.log2(camera().shape).max()))
In [20]: wavelet = pywt.Wavelet('haar')
In [21]: wavelet_coeffs = pywt.wavedec2(camera(), wavelet,
....: level=levels)
In [22]: compressed = pywt.waverec2(wavelet_coeffs[:8], wavelet)

与变换编码相似的是通过奇异值分解进行压缩的方法。 在这种情况下,我们将图像视为矩阵。 我们通过其奇异值表示它。 当我们忽略大量较小的奇异值,然后进行重构时,就会发生这种情况下的压缩。 有关此技术的示例,请阅读《数值和科学计算的学习科学》,第二版的第 3 章和用于线性代数的科学。
图片编辑
编辑的目的是更改数字图像,通常是为了改善其属性或将其转变为进一步处理的中间步骤。
让我们研究一下不同的编辑方法:
- 领域的转变
- 强度调整
- 影像还原
- 图像修复
领域的转变
在这种设置下,我们通过首先更改像素的位置来解决图像的转换:旋转,压缩,拉伸,漩涡,裁切,透视图控制等等。 完成对原始域中像素的转换后,我们将观察输出的大小。 如果此图像中的像素多于原始像素,则多余的位置将填充通过对给定数据进行插值获得的数值。 当然,我们确实对执行的插值类型有所控制。 为了更好地说明这些技术,我们将实际图像(例如,Lena)与作为棋盘格的域表示形式配对:
In [1]: import numpy as np, matplotlib.pyplot as plt
In [2]: from scipy.misc import lena; \
...: from skimage.data import checkerboard
In [3]: image = lena().astype('float')
...: domain = checkerboard()
In [4]: print image.shape, domain.shape
Out[4]: (512, 512) (200, 200)
调整大小
在进行图像和域的配对之前,我们必须确保它们都具有相同的大小。 一种快速的解决方法是重新调整两个对象的大小,或者只是调整其中一个图像的大小以匹配另一个图像的大小。 让我们从第一个选择出发,以便我们可以说明在skimage.transform模块中用于调整大小和缩放的两个函数的用法:
In [5]: from skimage.transform import rescale, resize
In [6]: domain = rescale(domain, scale=1024./200); \
...: image = resize(image, output_shape=(1024, 1024), order=3)
观察在调整大小的操作中我们如何请求双三次插值。
漩涡
要执行漩涡,我们从模块skimage.transform调用函数swirl:
提示
在本节的所有示例中,我们将在执行请求的计算后直观地呈现结果。 在所有情况下,提供图像的调用语法都是相同的。 对于给定的操作映射,我们发出命令display(mapping, image, domain),其中例程display定义如下:
def display(mapping, image, domain):
plt.figure()
plt.subplot(121)
plt.imshow(mapping(image))
plt.gray()
plt.subplot(122)
plt.imshow(mapping(domain))
plt.show()
为了简洁起见,我们不会在以下代码中包含此命令,但假定每次都调用它:
In [7]: from skimage.transform import swirl
In [8]: def mapping(img):
...: return swirl(img, strength=6, radius=512)
...:

几何变换
通过功能从模块scipy.ndimage或skimage.transform旋转,可以实现围绕任何位置(无论是在图像域的内部还是外部)的简单旋转。 它们本质上是相同的,但是scikit-image工具包中函数的语法更加用户友好:
In [10]: from skimage.transform import rotate
In [11]: def mapping(img):
....: return rotate(img, angle=30, resize=True, center=None)
....:
这样可以绕图像中心(center=None)逆时针旋转 30 度(angle=30)。 扩展输出图像的大小,以确保输出(resize=True)中存在所有原始像素:

旋转是所谓仿射变换的一种特殊情况-旋转与比例(每个维度一个),剪切和*移的组合。 仿射变换又是单应性的一种特殊情况(射影变换)。 库skimage.transform允许学习非常舒适的设置,而不是学习各种函数(针对每种几何变换)。 有一个通用函数(warp)被请求的几何变换调用并执行计算。 每个合适的几何变换都已使用合适的 Python 类进行了初始化。 例如,对于 x ,以围绕坐标为(512,-2048),比例因子分别为 2 和 3 个单位的点的逆时针旋转角度为 30 度执行仿射变换。 和 y 坐标,我们发出以下命令:
In [13]: from skimage.transform import warp, AffineTransform
In [14]: operation = AffineTransform(scale=(2,3), rotation=np.pi/6, \
....: translation = (512, -2048))
In [15]: def mapping(img):
....: return warp(img, operation)
....:

观察变换后的棋盘中的所有线如何*行或垂直-仿射变换会保留角度。
下面说明了单应性的效果:
In [17]: from skimage.transform import ProjectiveTransform
In [18]: generator = np.matrix('1,0,10; 0,1,20; -0.0007,0.0005,1'); \
....: homography = ProjectiveTransform(matrix=generator); \
....: mapping = lambda img: warp(img, homography)

观察与仿射变换不同的是,线如何不再全部*行或垂直。 现在所有垂直线都入射在一个点上。 所有水*线也入射在不同点。
例如,当我们需要执行透视控制时,同形异义词才真正有用。 例如,图像skimage.data.text明显倾斜。 通过选择我们认为是完美矩形的四个角(我们通过视觉检查来估计这样的矩形),我们可以计算出单应性,从而将给定的图像转换为没有任何角度的图像。 代表几何变换的 Python 类使我们可以非常轻松地执行此估算,如以下示例所示:
In [20]: from skimage.data import text
In [21]: text().shape
Out[21]: (172, 448)
In [22]: source = np.array(((155, 15), (65, 40),
....: (260, 130), (360, 95),
....: (155, 15)))
In [23]: mapping = ProjectiveTransform()
让我们估计将单点转换成给定点集为大小为 48 x 256 的完美矩形的单应性,该矩形以 512 x 512 大小的输出图像为中心。输出图像大小的选择取决于对角线的对角线长度 原始图像(约 479 像素)。 这样,在计算了单应性之后,输出可能包含原始图像的所有像素:
提示
观察到我们在源中两次包含了一个顶点。 对于下面的计算,这不是绝对必要的,但是它将使矩形的可视化更容易编码。 我们对目标矩形使用相同的技巧。
In [24]: target = np.array(((256-128, 256-24), (256-128, 256+24),
....: (256+128, 256+24), (256+128, 256-24),
....: (256-128, 256-24)))
In [25]: mapping.estimate(target, source)
Out[25]: True
In [26]: plt.figure(); \
....: plt.subplot(121); \
....: plt.imshow(text()); \
....: plt.gray(); \
....: plt.plot(source[:,0], source[:,1],'-', lw=1, color='red'); \
....: plt.xlim(0, 448); \
....: plt.ylim(172, 0); \
....: plt.subplot(122); \
....: plt.imshow(warp(text(), mapping,output_shape=(512, 512))); \
....: plt.plot(target[:,0], target[:,1],'-', lw=1, color='red'); \
....: plt.xlim(0, 512); \
....: plt.ylim(512, 0); \
....: plt.show()

提示
例如,需要其他更复杂的几何运算来修复渐晕以及摄影镜头产生的其他一些畸变。 传统上,一旦我们获取图像,我们就假定所有这些失真都存在。 通过了解用于拍照的设备的技术规格,我们可以自动纠正这些缺陷。 考虑到这一目的,在 SciPy 堆栈中,我们可以通过软件包lensfunpy( https:/ /pypi.python.org/pypi/lensfunpy )。
有关用法和文档的示例,请访问 http://pythonhosted.org/lensfunpy/api/ 上的lensfunpy API 参考。
强度调整
在此类别中,我们有一些操作只能遵循全局公式来修改图像的强度。 因此,通过使用纯 NumPy 运算,通过创建适应所需公式的矢量化函数,可以轻松地对所有这些运算进行编码。
例如,可以根据黑白摄影中的曝光来解释这些操作的应用。 因此,本节中的所有示例都适用于灰度图像。
我们主要通过三种方法来处理图像强度来增强图像:
- 直方图均衡
- 强度限幅/调整大小
- 对比度调整
直方图均衡
此设置的起点始终是强度直方图(或更准确地说,是像素强度值的直方图)的概念-一种功能,该功能指示在该图像中找到的每个不同强度值处的图像像素数。
例如,对于原始版本的 Lena,我们可以发出以下命令:
In [27]: plt.figure(); \
....: plt.hist(lena().flatten(), 256); \
....: plt.show()

直方图均衡化的操作通过以某种方式修改直方图以使大多数相关强度具有相同影响的方式来改善图像的对比度。 我们可以通过从模块skimage.exposure调用任何函数equalize_hist(纯直方图均衡)或equalize_adaphist(对比度受限的自适应直方图均衡( CLAHE )。
请注意,在对图像skimage.data.moon应用直方图均衡后,明显改善了:
提示
在以下示例中,我们在所有相关图像下方包括了相应的直方图,以进行比较。 执行此可视化的合适代码如下:
def display(image, transform, bins):
target = transform(image)
plt.figure()
plt.subplot(221)
plt.imshow(image)
plt.gray()
plt.subplot(222)
plt.imshow(target)
plt.subplot(223)
plt.hist(image.flatten(), bins)
plt.subplot(224)
plt.hist(target.flatten(), bins)
plt.show()
In [28]: from skimage.exposure import equalize_hist; \
....: from skimage.data import moon
In [29]: display(moon(), equalize_hist, 256)

强度限幅/调整大小
直方图上的峰表示存在特定强度,该强度明显比其相邻强度大。 如果我们希望隔离峰附*的强度,则可以对原始图像使用纯 NumPy 遮罩/剪切操作。 如果不需要存储结果,我们可以通过在库matplotlib.pyplot中使用命令clim来要求对结果进行快速可视化。 例如,要隔离 Lena 最高峰附*的强度(大约在 150 至 160 之间),我们可以发出以下命令:
In [30]: plt.figure(); \
....: plt.imshow(lena()); \
....: plt.clim(vmin=150, vmax=160); \
....: plt.show()
请注意,尽管此操作将强度的代表性范围从 256 减小到了 10,但如何为我们提供了一个新图像,该图像具有足够的信息来重建原始图像。 当然,我们也可以将此操作视为有损压缩方法:

对比增强
削波强度的一个明显缺点是失去了感知到的明暗对比。 为了克服这种损失,最好采用不减小范围大小的公式。 在许多符合该数学性质的可用公式中,最成功的公式是那些复制采集方法光学性质的公式。 我们研究以下三种情况:
- 伽玛校正:人类视觉遵循幂函数,对较暗色调之间的相对差异比较亮色调之间的相对差异更敏感。 采集设备捕获的每个原始图像可能分配太多的位或太多的带宽,以突出显示人类实际上无法区分。 同样,太少的位/带宽可能无法分配给图像的较暗区域。 通过操作此幂函数,我们能够处理正确的位数和带宽。
- S 形校正:与位数和带宽无关,通常希望保持感知的明暗对比。 然后根据从心理物理适应性实验的结果发展出来的经验对比增强模型设计 S 型重映射功能。
- 对数校正:这是一个纯粹的数学过程,旨在通过转换为对数范围来扩展自然低对比度图像的范围。
为了对图像执行伽玛校正,我们可以在模块skimage.exposure中使用功能adjust_gamma。 等效的数学运算是幂律关系output = gain * input^gamma。 当我们选择指数gamma=2.5而没有增益( gain=1.0 )时,观察结肠结肠染色显微照片的较亮区域的定义有所改善:
In [31]: from skimage.exposure import adjust_gamma; \
....: from skimage.color import rgb2gray; \
....: from skimage.data import immunohistochemistry
In [32]: image = rgb2gray(immunohistochemistry())
In [33]: correction = lambda img: adjust_gamma(img, gamma=2.5,
....: gain=1.)
请注意,显微照片右下部分的对比度有了很大的改善,从而可以更好地描述和区分观察到的物体:

苏木精复染的免疫组织化学染色。 该图像在显微镜和分子图像处理中心(CMMI)上获得。
为了根据给定的gain和cutoff系数执行 S 型校正,我们根据公式output = 1/(1 + exp*(gain*(cutoff - input)))在skimage.exposure中采用函数adjust_sigmoid。 例如,使用gain=10.0和cutoff=0.5(默认值),我们可以获得以下增强:
In [35]: from skimage.exposure import adjust_sigmoid
In [36]: display(image[:256, :256], adjust_sigmoid, 256)
请注意,增强图像中细胞壁的定义有所改善:

当增强图像频率的可视化时,我们已经在上一节中探讨了对数校正。 这等效于将np.log1p的矢量化版本应用于强度。 scikit-image工具包中的相应功能是子模块exposure中的adjust_log。
影像还原
在这种图像编辑类别中,目的是修复由于图像的后期处理或预处理,甚至消除由采集设备产生的失真而引起的损坏。 我们探索两种经典情况:
- 降噪
- 锐化和模糊
降噪
在数字图像处理中,根据定义,噪声是采集设备产生的强度(或颜色)的随机变化。 在所有可能的噪声类型中,我们承认以下四个关键情况:
- 高斯噪声:我们向每个像素添加从具有正态分布和固定均值的随机变量获得的值。 我们通常在图像的每个像素上允许相同的方差,但是根据位置更改方差是可行的。
- 泊松噪声:对于每个像素,我们添加一个从具有泊松分布的随机变量获得的值。
- 盐和胡椒粉:这是一种替换噪声,其中某些像素用零(黑色或胡椒)代替,而某些像素用 1(白色或盐)代替。
- 斑点:这是一种可乘的噪声,其中每个像素都由公式
output = input + n * input修改。 修饰符n的值是从固定*均值和方差均匀分布的随机变量获得的值。
为了模拟所有这些噪声,我们使用了模块skimage.util中的实用程序random_noise。 让我们以一个共同的形象来说明各种可能性:
In [37]: from skimage.data import camera; \
....: from skimage.util import random_noise
In [38]: gaussian_noise = random_noise(camera(), 'gaussian',
....: var=0.025); \
....: poisson_noise = random_noise(camera(), 'poisson'); \
....: saltpepper_noise = random_noise(camera(), 's&p',
....: salt_vs_pepper=0.45); \
....: speckle_noise = random_noise(camera(), 'speckle', var=0.02)
In [39]: variance_generator = lambda i,j: 0.25*(i+j)/1022\. + 0.001; \
....: variances = np.fromfunction(variance_generator,(512,512)); \
....: lclvr_noise = random_noise(camera(), 'localvar',
....: local_vars=variances)
在最后一个示例中,我们创建了一个函数,该函数根据到图像上角的距离分配0.001和0.026之间的差异。 当我们可视化skimage.data.camera的相应噪点版本时,我们看到,随着我们靠*图片的右下角,降级程度变得更强。
以下是相应的噪点图像的可视化示例:

降噪的目的是消除尽可能多的不想要的信号,因此我们获得的图像尽可能接*原始图像。 当然,诀窍是在没有任何噪声特性的事先知识的情况下这样做。
最基本的去噪方法是应用高斯或中值滤波器。 在上一节中,我们对它们进行了探讨。 前者以*滑滤波器(gaussian_filter)的形式出现,而后者在我们探索统计滤波器(median_filter)时进行了讨论。 它们都提供了不错的噪声消除功能,但同时也会引入不需要的伪像。 例如,高斯滤镜不会保留图像中的边缘。 如果需要保留纹理信息,也不建议使用这些方法中的任何一种。
我们在模块skimage.restoration中提供了一些更高级的方法,它们可以对具有特定属性的图像进行去噪定制:
denoise_bilateral:这是双边过滤器。 当保留边缘很重要时,它很有用。denoise_tv_bregman,denoise_tv_chambolle:如果我们需要总变化很小的去噪图像,我们将使用它。nl_means_denoising:所谓的非局部均值降噪。 此方法可确保对图像呈现纹理区域进行去噪的最佳结果。wiener和unsupervised_wiener:这是 Wiener-Hunt 反卷积。 当我们了解采集时的点扩展功能时,此功能很有用。
让我们以示例的方式展示这些方法之一在我们之前计算出的某些嘈杂图像上的性能:
In [40]: from skimage.restoration import nl_means_denoising as dnoise
In [41]: images = [gaussian_noise, poisson_noise,
....: saltpepper_noise, speckle_noise]; \
....: names = ['Gaussian', 'Poisson', 'Salt & Pepper', 'Speckle']
In [42]: plt.figure()
Out[42]: <matplotlib.figure.Figure at 0x118301490>
In [43]: for index, image in enumerate(images):
....: output = dnoise(image, patch_size=5, patch_distance=7)
....: plt.subplot(2, 4, index+1)
....: plt.imshow(image)
....: plt.gray()
....: plt.title(names[index])
....: plt.subplot(2, 4, index+5)
....: plt.imshow(output)
....:
In [44]: plt.show()
在每个嘈杂的图像下,我们采用非局部均值去噪后都给出了相应的结果。

如果我们用变换表示图像,也可以通过thresholding系数执行去噪。 例如,要使用 Biorthonormal 2.8 小波进行软阈值处理,我们将使用PyWavelets包:
In [45]: import pywt
In [46]: def dnoise(image, wavelet, noise_var):
....: levels = int(np.floor(np.log2(image.shape[0])))
....: coeffs = pywt.wavedec2(image, wavelet, level=levels)
....: value = noise_var * np.sqrt(2 * np.log2(image.size))
....: threshold = lambda x: pywt.thresholding.soft(x, value)
....: coeffs = map(threshold, coeffs)
....: return pywt.waverec2(coeffs, wavelet)
....:
In [47]: plt.figure()
Out[47]: <matplotlib.figure.Figure at 0x10e5ed790>
In [48]: for index, image in enumerate(images):
....: output = dnoise(image, pywt.Wavelet('bior2.8'),
....: noise_var=0.02)
....: plt.subplot(2, 4, index+1)
....: plt.imshow(image)
....: plt.gray()
....: plt.title(names[index])
....: plt.subplot(2, 4, index+5)
....: plt.imshow(output)
....:
In [49]: plt.show()
观察结果与使用先前方法获得的结果具有可比的质量:

锐化和模糊
在许多情况下会产生模糊的图像:
- 收购重点不正确
- 图像处理系统的运动
- 图像处理设备的点扩展功能(如电子显微镜中的)
- 图形艺术效果
对于模糊图像,我们可以通过将图像与相应内核进行卷积来复制点扩散函数的效果。 我们用于去噪的高斯滤波器以这种方式执行模糊处理。 在一般情况下,可以使用模块scipy.ndimage中的例程convolve与给定内核进行卷积。 例如,对于在 10 x 10 正方形上受支持的恒定内核,我们可以执行以下操作:
In [50]: from scipy.ndimage import convolve; \
....: from skimage.data import page
In [51]: kernel = np.ones((10, 10))/100.; \
....: blurred = convolve(page(), kernel)
为了模拟运动产生的模糊效果,我们可以使用此处创建的内核进行卷积:
In [52]: from skimage.draw import polygon
In [53]: x_coords = np.array([14, 14, 24, 26, 24, 18, 18]); \
....: y_coords = np.array([ 2, 18, 26, 24, 22, 18, 2]); \
....: kernel_2 = np.zeros((32, 32)); \
....: kernel_2[polygon(x_coords, y_coords)]= 1.; \
....: kernel_2 /= kernel_2.sum()
In [54]: blurred_motion = convolve(page(), kernel_2)

为了在了解退化过程时逆转卷积的影响,我们执行了反卷积。 例如,如果我们了解点扩散函数,则在模块skimage.restoration中,我们将实现 Wiener 滤波器(wiener),无监督 Wiener 滤波器(unsupervised_wiener)和 Lucy-Richardson 反卷积(richardson_lucy)。
我们通过应用维纳滤波器对模糊图像执行反卷积。 注意文本可读性的巨大改进:
In [55]: from skimage.restoration import wiener
In [56]: deconv = wiener(blurred, kernel, balance=0.025, clip=False)

修补
我们将修复定义为替换图像数据中丢失或损坏的部分(主要是较小的区域或去除较小的缺陷)。
目前正在进行各种努力,以包括用于在 skimage 中进行修补的 Crimini 算法的实现。 直到那一天到来,SciPy 堆栈之外还有其他两个选项-Alexandru Telea 的快速行进方法的实现和基于流体动力学(特别是 Navier-Stokes 方程)的实现。 可以从 OpenCV 的imgproc模块中的例程inpaint调用这两种实现。 我们使用 Telea 的算法来说明此技术的强大功能:将已删除区域的棋盘格skimage.data.checkerboard视为测试图像。
In [57]: from skimage.data import checkerboard
In [58]: image = checkerboard(); \
....: image[25:100, 25:75] = 0.
In [59]: mask = np.zeros_like(image); \
....: mask[25:100, 25:75] = 1.
In [60]: from cv2 import inpaint, INPAINT_TELEA, INPAINT_NS
In [61]: inpainted = inpaint(image, mask, 1, INPAINT_TELEA)

结果说明了修复的工作原理-从附*的已知值获得像素的强度。 因此,不足为奇的是,尽管保留了图像的几何形状,但修复的区域没有计算出正确的强度集,而是计算出了最合逻辑的强度。
提示
有关 OpenCV-Python 的图像处理模块imgproc的更多信息,请遵循 http://docs.opencv.org/modules/imgproc/doc/imgproc.html 上的 API 参考。
修复对于从图片中删除不需要的对象非常有用。 例如,在页面图像skimage.data.page中,观察去除包含换行符的大区域并使用 Navier-Stokes 算法对其进行修复的效果:
In [62]: image = page(); \
....: image[36:46, :] = image[140:, :] = 0
In [63]: mask = np.zeros_like(image); \
....: mask[36:46, :] = mask[140:, :] = 1
In [64]: inpainted = inpaint(image, mask, 5, INPAINT_NS)

图像分析
本部分的目的是从图像中提取信息。 我们将重点关注两种情况:
- 影像结构
- 物体识别
影像结构
目的是使用简单的结构表示图像的内容。 我们仅关注一种情况:图像分割。 我们鼓励读者探索其他设置,例如quadtree分解。
分割是一种通过分割为多个对象(段)来表示图像的方法; 他们每个人都有一些共同的财产。
在二进制图像的情况下,我们可以通过加标签的过程来完成此操作,如上一节中所示。 让我们通过由 30 个随机磁盘组成的人工图像,在 64 x 64 画布上重现该技术:
In [1]: import numpy as np, matplotlib.pyplot as plt
In [2]: from skimage.draw import circle
In [3]: image = np.zeros((64, 64)).astype('bool')
In [4]: for k in range(30):
...: x0, y0 = np.random.randint(64, size=(2))
...: image[circle(x0, y0, 3)] = True
...:
In [5]: from scipy.ndimage import label
In [6]: labels, num_features = label(image)
变量labels可以看作是另一幅图像,其中原始图像中找到的每个不同对象都被赋予了不同的编号。 图像的背景也被认为是另外一个对象,并以数字 0 作为标签。 它的视觉表示(在下图的右侧)呈现了图像中的所有对象,每个对象具有不同的颜色:

对于灰度或彩色图像,分割过程更加复杂。 我们通常可以将此类图像缩小为相关区域的二进制表示形式(通过形态学处理后的某种阈值运算),然后应用标记过程。 但这并不总是可能的。 以硬币图像skimage.data.coins为例。 在此图像中,背景与背景中的许多硬币具有相同的强度范围。 阈值操作将导致无法有效分割。
我们有更多高级选项:
- 使用像素强度/颜色之间的差异作为像素之间的距离的聚类方法
- 基于压缩的方法
- 基于直方图的方法,其中我们使用图像直方图中的峰和谷将其分成多个部分
- 区域生长方法
- 基于偏微分方程的方法
- 变体方法
- 图分割方法
- 分水岭方法
从 SciPy 堆栈的角度来看,我们主要有两种选择,一种是scipy.ndimage中的工具的组合,另一种是模块skimage.segmentation中的分割程序。
提示
通过绑定到功能强大的库 Insight 细分和注册工具包( ITK ),还有非常强大的实现集。 有关此库的一般信息,最好的资源是其官方网站 http://www.itk.org/ 。
我们在其之上使用了简化的包装器构建:SimpleITK的 Python 发行版。 该软件包通过绑定到 Python 函数,带来了 ITK 的大多数功能。 有关文档,下载和安装,请访问 http://www.simpleitk.org/ 。
不幸的是,在编写本书时,安装非常棘手。 成功的安装在很大程度上取决于您的 Python 安装,计算机系统,已安装的库等。
让我们以示例的方式看一下其中一些技术在特别棘手的图像skimage.data.coins上的用法。 例如,要执行基于直方图的简单分割,我们可以按照以下步骤进行:
In [8]: from skimage.data import coins; \
...: from scipy.ndimage import gaussian_filter
In [9]: image = gaussian_filter(coins(), sigma=0.5)
In [10]: plt.hist(image.flatten(), bins=128); \
....: plt.show()
提示
请注意,我们是如何首先通过使用球形高斯滤波器进行卷积来对原始图像进行*滑处理的。 这是消除可能的有害信号并获得更清晰结果的标准程序。

在强度80和85的峰值之间,强度80周围似乎有一个清晰的山谷。 在强度112周围还有一个谷,随后是强度123的峰。 强度137附*还有一个谷,强度160之后是最后一个峰。 我们使用此信息来创建四个细分:
In [11]: level_1 = coins()<=80; \
....: level_2 = (coins()>80) * (coins()<=112); \
....: level_3 = (coins()>112) * (coins()<=137); \
....: level_4 = coins()>137
In [12]: plt.figure(); \
....: plt.subplot2grid((2,4), (0,0), colspan=2, rowspan=2); \
....: plt.imshow(coins()); \
....: plt.gray(); \
....: plt.subplot2grid((2,4),(0,2)); \
....: plt.imshow(level_1); \
....: plt.axis('off'); \
....: plt.subplot2grid((2,4),(0,3)); \
....: plt.imshow(level_2); \
....: plt.axis('off'); \
....: plt.subplot2grid((2,4), (1,2)); \
....: plt.imshow(level_3); \
....: plt.axis('off'); \
....: plt.subplot2grid((2,4), (1,3)); \
....: plt.imshow(level_4); \
....: plt.axis('off'); \
....: plt.show()

在对第四级进行稍加修改后,我们将获得不错的细分:
In [13]: from scipy.ndimage.morphology import binary_fill_holes
In [14]: level_4 = binary_fill_holes(level_4)
In [15]: labels, num_features = label(level_4)

结果不是最佳的。 该过程没有在第五列中很好地分割某些硬币,但主要是在最低的行中。
如果我们为有兴趣获得的每个细分市场提供标记,则可以进行改进。 例如,我们可以假设我们知道那 24 个硬币的位置内至少有一个点。 然后,我们可以为此目的使用分水岭变换。 在模块scipy.ndimage中,我们基于迭代森林变换实现了此过程:
In [17]: from scipy.ndimage import watershed_ift
In [18]: markers_x = [50, 125, 200, 255]; \
....: markers_y = [50, 100, 150, 225, 285, 350]
In [19]: markers = np.zeros_like(image).astype('int16'); \
....: markers_index = [[x,y] for x in markers_x for y in
....: markers_y]
In [20]: for index, location in enumerate(markers_index):
....: markers[location[0], location[1]] = index+5
....:
In [21]: segments = watershed_ift(image, markers)

并非所有硬币都已正确分割,但已更正的硬币定义更好。 为了进一步改善加水印的结果,我们可以很好地研究标记的准确性,或者为每个所需的片段添加一个以上的点。 请注意,当我们每枚硬币仅添加一个标记以更好地分割三个遗失的硬币时会发生什么:
In [23]: markers[53, 273] = 9; \
....: markers[130, 212] = 14; \
....: markers[270, 42] = 23
In [24]: segments = watershed_ift(image, markers)

我们可以通过采用图分割方法(例如随机游走器)进一步改善分割效果:
In [26]: from skimage.segmentation import random_walker
In [27]: segments = random_walker(image, markers)

此过程可以将图像正确地分成 24 个高分辨区域,但不能很好地分辨背景。 为了解决这种情况,我们用a-1手动标记了我们认为是背景的那些区域。 我们可以使用先前计算的蒙版level_1和level_2-它们可以清楚地表示图像背景,以实现以下目的:
In [29]: markers[level_1] = markers[level_2] = -1
In [30]: segments = random_walker(image, markers)

对于其他分割技术,请浏览模块skimage.segmentation中的不同例程。
物体识别
出现了许多可能性。 给定图像,我们可能需要收集简单的几何特征的位置,例如边缘,拐角,线性,圆形或椭圆形,多边形,斑点等。 我们可能还需要找到更复杂的对象,例如面,数字,字母,*面,坦克等。 让我们研究一些示例,我们可以轻松地从 SciPy 堆栈中进行编码。
边缘检测
可以在模块skimage.feature中找到 Canny 边缘检测器的实现。 此实现对输入图像进行*滑处理,然后执行垂直和水* Sobel 运算符,以帮助提取边缘:
In [32]: from skimage.feature import canny
In [33]: edges = canny(coins(), sigma=3.5)

线,圆和椭圆检测
为了检测这些基本的几何形状,我们借助了霍夫变换。 在 OpenCV 的skimage.transform模块和imgproc模块中都可以找到可靠的实现。 让我们通过跟踪人造二进制图像的这些对象来检查前者中例程的用法。 让我们放置一个以中心(10, 10)和半径9和5(与坐标轴*行)的椭圆,一个以中心(30, 35)和半径8为中心的圆,以及两个点之间的线( 0, 3)和(64, 40):
In [35]: from skimage.draw import line, ellipse_perimeter, \
....: circle_perimeter
In [36]: image = np.zeros((64, 64)).astype('bool'); \
....: image[ellipse_perimeter(10, 10, 9, 5)] = True; \
....: image[circle_perimeter(30, 35, 15)] = True; \
....: image[line(0, 3, 63, 40)] = True
为了对行使用霍夫变换,我们计算相应的 H 空间(累加器),并提取其峰的位置。 在 Hough 变换的线型情况下,累加器的轴表示角度theta和到直线的 Hesse 法线形式的原点r的距离 r = x cos(θ)+ y sin( θ)。 然后,累加器中的峰值表明给定图像中最相关的线的存在:
In [37]: from skimage.transform import hough_line, hough_line_peaks
In [38]: Hspace, thetas, distances = hough_line(image); \
....: hough_line_peaks(Hspace, thetas, distances)
Out[38]:
(array([52], dtype=uint64),
array([-0.51774851]),
array([ 3.51933702]))
此输出意味着在图像的霍夫变换的 H 空间中只有一个重要的峰。 该峰对应于一条直线,其黑森角为-0.51774851弧度,距原点为3.51933702单位:
3.51933702 = cos(-0.51774851)x + sin(-0.51774851)和
让我们一起看原始图像和检测到的行:
In [39]: def hesse_line(theta, distance, thickness):
....: return lambda i, j: np.abs(distance - np.cos(theta)*j \
....: - np.sin(theta)*i) < thickness
....:
In [40]: peak, theta, r = hough_line_peaks(Hspace, thetas, distances)
In [41]: detected_lines = np.fromfunction(hesse_line(theta, r, 1.),
....: (64, 64))
提示
注意在hesse_line定义中坐标i和j的作用反转。 为什么我们必须执行这种人为改变的坐标?

圆和椭圆的检测遵循类似的原理,即在某些 H 空间中计算累加器并跟踪其峰值。 例如,如果我们正在寻找半径等于15的圆,并且希望恢复其圆心,则可以按照以下方式发出一些信息:
In [43]: from skimage.transform import hough_circle
In [44]: detected_circles = hough_circle(image,radius=np.array([15]))
In [45]: np.where(detected_circles == detected_circles.max())
Out[45]: (array([0]), array([30]), array([35]))
数组detected_circles具有形状(1, 64, 64)。 因此,最后一个输出的第一个索引无关紧要。 报告的其他两个索引指示检测到的圆的中心正好(30, 35)。
斑点检测
我们可以将斑点视为图像的所有像素共享相同属性的区域。 例如,在进行细分之后,找到的每个细分在技术上都是斑点。
为此,模块skimage.feature中有一些相关的例程,blob_doh(一种基于 Hessians 行列式的方法),blob_dog(一种基于高斯差分的方法)和blob_log(一种基于 Laglacian 的拉普拉斯算子的方法) 高斯人)。 第一种方法可确保提取更多样本,并且比其他两种方法更快:
In [46]: from skimage.data import hubble_deep_field; \
....: from skimage.feature import blob_doh; \
....: from skimage.color import rgb2gray
In [47]: image = rgb2gray(hubble_deep_field())
In [48]: blobs = blob_doh(image)
In [49]: plt.figure(); \
....: ax1 = plt.subplot(121); \
....: ax1.imshow(image); \
....: plt.gray(); \
....: ax2 = plt.subplot(122); \
....: ax2.imshow(np.zeros_like(image))
Out[49]: <matplotlib.image.AxesImage at 0x105356d10>
In [50]: for blob in blobs:
....: y, x, r = blob
....: c = plt.Circle((x, y),r,color='white',lw=1,fill=False)
....: ax2.add_patch(c)
....:
In [51]: plt.show()

拐角检测
拐角是两个未对齐边缘相交的位置。 这是图像分析中最有用的操作之一,因为许多复杂的结构都需要仔细定位这些特征。 应用范围从复杂的对象或运动识别到视频跟踪,3D 建模或图像配准。
在模块skimage.feature中,我们实现了一些最著名的算法来解决此问题:
- FAST 拐角检测(来自加速段测试的功能):
corner_fast - Förstner 拐角检测(用于亚像素精度):
corner_foerstner - 哈里斯角点测度响应(基本方法):
corner_harris - 厨房和 Rosenfeld 角点测量响应:
corner_kitchen_rosenfeld - Moravec 角点测量响应:这是简单且快速的方法,但无法检测相邻边缘不完全笔直的角点:
corner_moravec - Kanade-Tomasi 角点测量响应:
corner_shi_tomasi
我们还有一些实用程序来确定角的方向或其子像素位置。
让我们探讨skimage.data.text中拐角的出现:
In [52]: from skimage.data import text; \
....: from skimage.feature import corner_fast, corner_peaks, \
....: corner_orientations
In [53]: mask = np.ones((5,5))
In [54]: corner_response = corner_fast(text(), threshold=0.2); \
....: corner_pos = corner_peaks(corner_response); \
....: corner_orientation = corner_orientations(text(), corner_pos,
....: mask)
In [55]: for k in range(5):
....: y, x = corner_pos[k]
....: angle = np.rad2deg(corner_orientation[k])
....: print "Corner ({}, {}) orientation {}".format(x,y,angle)
....:
Corner (178, 26) orientation -146.091580713
Corner (257, 26) orientation -139.929752986
Corner (269, 30) orientation 13.8150253413
Corner (244, 32) orientation -116.248065313
Corner (50, 33) orientation -51.7098368078
In [56]: plt.figure(); \
....: ax = plt.subplot(111); \
....: ax.imshow(text()); \
....: plt.gray()
In [57]: for corner in corner_pos:
....: y, x = corner
....: c = plt.Circle((x, y), 2, lw=1, fill=False, color='red')
....: ax.add_patch(c)
....:
In [58]: plt.show()

超越几何实体
对象检测不限于几何实体。 在本小节中,我们探索一些跟踪更复杂对象的方法。
在二进制图像的范围内,简单的关联通常足以实现某种程度不错的对象识别。 以下示例跟踪在描绘 Miguel de Cervantes 的 Don Quixote 的第一段的图像上字母e的大多数实例。 此图片的 tiff 版本已放置在 https://github.com/blancosilva/Mastering-Scipy/tree/master/chapter9 处:
In [59]: from scipy.misc import imread
In [60]: quixote = imread('quixote.tiff'); \
....: bin_quixote = (quixote[:,:,0]<50); \
....: letter_e = quixote[10:29, 250:265]; \
....: bin_e = bin_quixote[10:29, 250:265]
In [61]: from scipy.ndimage.morphology import binary_hit_or_miss
In [62]: x, y = np.where(binary_hit_or_miss(bin_quixote, bin_e))
In [63]: plt.figure(); \
....: ax = plt.subplot(111); \
....: ax.imshow(bin_quixote)
Out[63]: <matplotlib.image.AxesImage at 0x113dd8750>
In [64]: for loc in zip(y, x):
....: c = plt.Circle((loc[0], loc[1]), 15, fill=False)
....: ax.add_patch(c)
....:
In [65]: plt.show()

文本呈现中的小瑕疵或大小的微小变化使关联成为一种瑕疵检测机制。
通过 OpenCV 模块imgproc中的例程matchTemplate,可以将其应用于灰度或彩色图像:
In [66]: from cv2 import matchTemplate, TM_SQDIFF
In [67]: detection = matchTemplate(quixote, letter_e, TM_SQDIFF); \
....: x, y = np.where(detection <= detection.mean()/8.)
In [68]: plt.figure(); \
....: ax = plt.subplot(111); \
....: ax.imshow(quixote)
Out[68: <matplotlib.image.AxesImage at 0x26c7da890>]
In [69]: for loc in zip(y, x):
....: r = pltRectangle((loc[0], loc[1]), 15, 19, fill=False)
....: ax.add_patch(r)
....:
In [70]: plt.show()

现在已正确检测到所有字母e。
让我们以更复杂的对象检测案例结束本章。 我们将使用基于 Haar 特征的级联分类器:这是一种算法,该算法应用了基于机器学习的方法来从一些训练数据中检测面部和眼睛。
首先,在您的 OpenCV 安装文件夹中找到子文件夹haarcascades。 例如,在我的 Anaconda 安装中,这是在/anaconda/pkgs/opencv-2.4.9-np19py27_0/share/OpenCV/haarcascades。 在该文件夹中,我们将需要用于正面(haarcascade_frontalface_default.xml)和眼睛(haarcascade_eye.xml)的数据库:
In [71]: from cv2 import CascadeClassifier; \
....: from skimage.data import lena
In [72]: face_cascade = CascadeClassifier('haarcascade_frontalface_default.xml'); \
....: eye_cascade = CascadeClassifier('haarcascade_eye.xml')
In [73]: faces = face_cascade.detectMultiScale(lena()); \
....: eyes = eye_cascade.detectMultiScale(lena())
In [74]: print faces
[[212 199 179 179]]
In [75]: print eyes
[[243 239 53 53]
[310 247 40 40]]
结果是检测到一张脸和两只眼睛。 让我们在视觉上将它们放在一起:
In [76]: plt.figure(); \
....: ax = plt.subplot(111); \
....: ax.imshow(lena())
Out[76]: <matplotlib.image.AxesImage at 0x269fabed0>
In [77]: x, y, w, ell = faces[0]; \
....: r = plt.Rectangle((x, y), w, ell, lw=1, fill=False); \
....: ax.add_patch(r)
Out[77]: <matplotlib.patches.Rectangle at 0x26a837cd0>
In [78]: for eye in eyes:
....: x,y,w, ell = eye
....: r = plt.Rectangle((x,y),w,ell,lw=1,fill=False)
....: ax.add_patch(r)
....:
In [79]: plt.show()

摘要
在本章中,我们已经看到了 SciPy 堆栈如何帮助我们解决数字图像处理中的许多问题,从有效地表示数字图像到有效地存储,压缩和处理,修改,还原或分析它们。 尽管本章肯定详尽无遗,但本章还是从头开始的,但这是工程学这个充满挑战的领域的表面。 您可以轻松地写出另外 400 页专门用于该主题的页面,我邀请您进一步研究模块scipy.ndimage,映像工具包skimage以及库 OpenCV 或 SimpleITK 的 Python 绑定的可能性。
本章还关闭了我对掌握 SciPy 堆栈的含义的了解。 实际上,这种愿景仅关注科学应用和所需例程背后的数学理论之间的关系。 例如,尚未做出任何努力来解决通过与其他语言绑定来加速代码的技术。 尽管这是一个有趣且相关的主题,但我还是参考了有关计算机科学的其他更多技术专着。


浙公网安备 33010602011771号