如何在 Python 中绘制曼德勃罗集

2025 年 1 月 11 日 | 14 分钟阅读

本教程将讨论一个有趣的涉及复杂数字的 Python 项目。我们将学习分形,并使用 Python 的 Matplotlib 和 Pillow 库,通过曼德尔布罗集的图示制作令人惊叹的艺术品。我们还将找出这个分形是如何创建的及其意义,以及它与其它分形的关系。

理解面向对象编程原则和递归的概念将使我们能够充分利用 Python 的表达性语法,并编写易于阅读的数学公式式代码。要理解创建分形的算法,我们需要熟悉处理对数、集合论和重复函数等复杂的数学概念。但是,不要让这些基础知识成为障碍,因为我们将能够跟随并根据自己的喜好创作艺术品!

如何理解曼德尔布罗集

在 Python 中绘制曼德尔布罗集的目标是可视化一个基于复数的著名分形图案。曼德尔布罗集由复平面中在简单数学函数的重复迭代下保持有界限的点组成。

要生成曼德尔布罗集,首先要定义一个复数网格。对于每个点(复数),应用迭代过程,使用公式:z = z² + c,其中 z 从 0 开始,c 是表示该点的复数。迭代对每个点持续进行,如果在一定次数的迭代后 z 的幅度保持在一个阈值(通常为 2)以下,则该点属于曼德尔布罗集。

一个点在不“逃逸”超出阈值的情况下能承受的迭代次数越多,它就越可能是集合的一部分。这些点以黑白或彩色可视化方式绘制,其中每种颜色表示该点逃逸的速度。

为了可视化该集合,使用 Matplotlib 或 PIL 等 Python 库来绘制这些点。结果是一个美丽、复杂的碎形图案,以各种尺度重复出现,这是曼德尔布罗集无限复杂性的特征。

分形几何的标志

如果用户不熟悉这个名字,他们可能在此之前已经看过一些属于曼德尔布罗集的令人惊叹的可视化。它是一个复数集合,当在复杂平面上显示时,形成一个复杂而独特的图案。这个图案可能是最著名的分形,并在 20 世纪后半叶催生了分形几何的概念。

How to Draw the Mandelbrot Set in Python

曼德尔布罗集的出现得益于技术的进步。据信,它是一位名叫本华·曼德尔布罗特的数学家的作品。他在 IBM 工作,能够接触到当时可以进行大量数字运算的计算机。如今,我们足不出户,只用 Python 就可以研究分形!曼德尔布罗集的这一发现得益于技术进步。

分形是各种大小无限重复的图案。哲学家们几个世纪以来一直在争论无限的存在,分形在我们的世界中也有类似之处。这是一个在自然界中发生的极其普遍的现象。例如,这种罗马花椰菜不是无限的,但它是一种自相似的形式,因为植物的每个部分看起来都像整体,只是更小。

How to Draw the Mandelbrot Set in Python

自相似性通常使用递归概念进行数学定义。实际上,曼德尔布罗集并非自相似,因为它是由两个略有不同的较小维度的集合版本组成的。然而,它可以被描述为复数域中的递归函数。

迭代稳定性的边界

曼德尔布罗集指的是复数 c 的集合。无限序列,如 z0, z1, ..., zn, ..., 保持有界。此外,该序列中任何复数的大小上限永远不会超过。以下递归公式定义了曼德尔布罗序列

How to Draw the Mandelbrot Set in Python

简单来说,为了确定某个复数 **c** 是否属于曼德尔布罗集,我们需要将该数代入下面的公式。从现在开始,我们输入的数在序列中将保持不变。这个序列的初始元素 z0 将始终为零。为了计算下一个元素 zn+1,我们将通过对前一个元素 zn 求平方来继续。然后,我们将添加起始数 c,以创建反馈循环。

通过观察函数序列及其行为,我们将能够将复数 c 分类为曼德尔布罗集的一部分或不属于。做出选择是基于我们设定的确定性水平的任意决定,因为更多的组件可以对 c 的数量提供精确的结论。序列是无限长的。但是,我们必须在我们能够停止的时候停止计算其元素。

对于复数,我们可以将这个过程可视化为二维。但是,为了过程的简单性,我们现在最好只看实数。如果我们要用 Python 应用上面的方程,它会是这样的

我们的 z() 函数返回序列的第 n 个元素。这就是它将元素的索引 n 作为第一个参数的原因。第二个参数“c”可以被视为您正在尝试测试的固定数字。由于递归,该函数将继续无限期地调用自身。为了打破重复调用的链,对基本情况进行条件检查,使用一个已知的立即解决方案——零。

测试我们的新公式,以确定当 c = 1 时序列的前十个元素,并观察会发生什么

输出

z(0) = 0
z(1) = 1
z(2) = 2
z(3) = 5
z(4) = 26
z(5) = 677
z(6) = 458330
z(7) = 210066388901
z(8) = 44127887745906175987802
z(9) = 1947270476915296449559703445493848930452791205   

请注意这些序列组件的快速增长。它揭示了 c 1 构成的一些信息。特别是这必须符合曼德尔布罗集,因为其所基于的序列没有界限。

有时,迭代方法可能比递归方法更有效。下面是一个替代函数,它生成具有指定输入值 c 的无限序列

sequence() 函数返回一个元素序列。sequence() 函数返回一个生成器对象,通过循环无限地提供序列的连续组件。由于它不提供元素的索引,因此您可以识别它们并在一定次数的迭代后停止它。

输出

z(0) = 0
z(1) = 1
z(2) = 2
z(3) = 5
z(4) = 26
z(5) = 677
z(6) = 458330
z(7) = 210066388901
z(8) = 44127887745906175987802
z(9) = 1947270476915296449559703445493848930452791205   

结果与之前的版本相同。然而,生成器函数允许我们通过惰性评估算法更有效地确定序列元素。此外,迭代消除了需要已计算序列元素的冗余函数调用。这意味着我们未来不会超出最大递归元素数量。

大多数数字会导致这个序列发散到无穷大。但是,有些数字会使序列稳定,通过使序列收敛到一个值或保持在某个区间内。有些数字会通过在相同值之间来回交替来确保序列持续稳定。稳定和规律稳定的值构成了所谓的曼德尔布罗集。

例如,添加整数 c = 1 将使序列无限膨胀,如你所知。然而,值 c = -1 会使其在 0 和 1 之间频繁跳动,而组合 c = 0 会导致序列只有一个数字

元素c = -1c = 0c = 1
z0000
z1-101
z2002
z3-105
z40026
z5-10677
z600458,330
z7-10210,066,388,901

很难确定哪些数字可以认为是稳定的,哪些不是,因为它对被测试值 c 的微小变化几乎不敏感。如果我们将实数放置在复杂平面上,那么我们将观察到以下图案出现

How to Draw the Mandelbrot Set in Python

该图像是通过对每个像素使用递归公式至少 20 次生成的,每个像素代表许多值。如果确定所得复数的值在所有迭代后仍然很小,则相应的像素将被着色为黑色。但是,当幅度大于半径 2 时,该过程结束并跳过当前像素。

令人惊讶的是,一个仅需要乘法和加法的简单公式可以创造出如此复杂的结构。但这并非全部。我们已经发现,我们可以使用这个公式并将其应用于创造无限不同的分形。

朱利亚集地图

很难在不提及曼德尔布罗集的情况下谈论它,也很难不提及几十年前法国数学家加斯顿·朱利亚在没有计算机协助的情况下发现的朱利亚集。朱利亚集和曼德尔布罗集密切相关,因为它们可以使用相同的公式,但起始条件不同。

只有一个曼德尔布罗集;却有许多朱利亚集。我们过去从 z0 = 0 开始序列。然后,我们将系统地检查一些任意复杂的数字,例如 c,以确定它是否是一个成员。相比之下,为了确定给定数字是否属于朱利亚序列,我们必须选择该数字作为序列的起点,然后选择另一个数字作为 c 参数。

以下是根据我们正在研究的集合,公式定义的快速概述

任期曼德尔布罗集朱利亚集
z00候选值
C候选值固定常数

在第一种情况下,c 代表曼德尔布罗集的可能成员,并且是唯一需要的输入值,因为 Z0 保持设置为零。但是,当我们在 Julia 模式下使用该公式时,每个术语的含义都会改变。在这种情况下,c 作为确定整个 Julia 集形式和形状的参数,而值 z0 将成为我们的主要焦点。和以前一样,Julia 集的公式只需要两个输入值。

我们可以修改我们之前定义的函数,使其更通用。这将允许我们创建从任何点开始而不是总是从零开始的无限序列。

由于此行中突出显示参数的默认值,我们仍然可以像以前一样使用此函数,因为不需要 z。此外,我们可以更改序列开始的点。在定义曼德尔布罗集或朱利亚集的函数包装器后,我们可能会获得更多清晰度

每个函数都将返回一个根据我们首选起始条件调整的生成器对象。用于确定单个值是否属于朱利亚集的原则与您之前观察到的曼德尔布罗集相似。简而言之,我们需要重复该过程,然后观察其随时间的演变。

本华·曼德尔布罗特在他的研究中一直在研究朱利亚集。他特别感兴趣的是找到那些导致所谓的“连通朱利亚集”的 c 值,而不是它们的非连通对应物。这些被称为法图集,其特征是当在复数平面上观察时,由无数碎片组成的尘埃

How to Draw the Mandelbrot Set in Python

众所周知,将曼德尔布罗集的任何成员代入递归公式将导致一系列收敛的复数。左上角的图像显示了一个连通的朱利亚集,它由公式 c = 0.25 导出,该公式是曼德尔布罗集的一部分。在这种情况下,数字收敛到 0.5。对数字 c 的微小改变可能会将你的朱利亚集变成尘埃,并导致序列分裂为无穷大。

连通的朱利亚集与使用上述递归公式产生稳定序列的 c 值相关。我们可以说本华·曼德尔布罗特正在寻找允许迭代稳定性的边界,或者说是一张朱利亚集地图,它将揭示这些集在哪里是连通的,在哪里不是。

使用 Python 的 Matplotlib 绘制曼德尔布罗集

有多种方法可以使用 Python 显示曼德尔布罗集。它们可以避免在世界坐标和像素坐标之间进行转换。如果用户熟悉使用 NumPy 库和 Matplotlib,这两个库结合起来将提供最简单的可视化分形方法之一。

要创建候选初始列表,我们可以使用 **np.linspace()**,它在指定范围内创建均匀间隔的数字

上面描述的函数将生成一个二维复数数组,该数组被包含在由四个变量定义的矩形空间中。这些参数是 x_min 和 x_max,它们确定水平方向的边界。相比之下,y_min 和 y_max 在垂直方向上是相同的。第五个参数,像素密度,是确定每单位像素数量的参数。

我们现在可以使用这个由复杂数字组成的数组,并使用众所周知的递归计算公式来确定哪些数字是稳定的,哪些不是。由于 NumPy 的向量化,矩阵可以作为一个元素 c 传递给矩阵,然后对每个元素执行计算,而无需编写显式循环

代码中突出显示的那行在每次重复时都会对矩阵 c 的所有元素执行。由于 Z 和 c 最初具有不同的维度,NumPy 会使用广播来扩展后者,使它们具有相同的形式。该函数在结果数组 z 上创建一个由布尔值组成的 2D 图像。每个值都与此时序列的稳定性相关。

为了利用矢量化计算,此代码中的循环会无限地继续添加和平方数字,无论它们之前有多大。这并不理想,因为数字通常会很早就开始发散,这使得大部分计算效率低下。

此外,快速增长的数字可能导致溢出错误。NumPy 会检测导致问题的溢出,并在标准错误流 (stderr) 中警告用户。如果我们希望阻止这些警告,我们可以在使用程序之前设置过滤器

经过设定的迭代次数后,矩阵中每个数字的幅度将保持在或超过两个的阈值。如果不是,它们很可能是曼德尔布罗集的成员。它们可以使用 Matplotlib 进行可视化。

低分辨率散点图

可视化曼德尔布罗集的一种简单方法是使用散点图,它显示变量之间的关系。由于复数是物理或实部和虚部的组合,因此将它们分解为单独的数组非常适合使用散点图。

首先,我们必须将布尔安全掩码转换为创建序列的复数。我们可以使用 NumPy 的掩码过滤来实现这一点

此函数返回一个一维数组,仅包含那些稳定的复数,因此属于曼德尔布罗特群。如果我们的函数在上一节中描述,我们将能够使用 Matplotlib 创建散点图。请务必在文件顶部包含我们需要的导入语句。

这将把绘图界面带到当前命名空间。我们现在可以计算我们的信息并绘制它

complex_matrix() 方法准备一个矩形复数数组,该数组在 x 方向上范围从 -1.5 到 0.25。它在 y 方向上在 -2 和 2 之间。find_members() 的以下操作仅通过曼德尔布罗集中的数字。然后,plot.scatter() 绘制集合,此外,plot.show() 将显示此图片

How to Draw the Mandelbrot Set in Python

这是数学历史的重演!它长 749 点,类似于本华·曼德尔布罗特几年前使用点阵打印机创建的原始 ASCII 打印输出。我们可以调整像素密度和重复次数,以确定它们如何影响最终结果。

高分辨率黑白可视化

为了更精确地可视化曼德尔布罗特图的黑白图像,我们可以增加散点图上的像素数量,直到单个点难以区分。使用二值颜色映射创建布尔稳定掩码,我们还可以使用 Matplotlib 的 plot.imshow() 函数。

您的当前代码只需要少量更改

确保我们将像素密度增加到高值,例如 612。之后,我们可以删除对 get_members() 函数的任何回调,并用 plot.imshow() 替换散点图以图像形式显示数据。如果一切按计划进行,我们应该能够看到曼德尔布罗特的集合:曼德尔布罗特集

How to Draw the Mandelbrot Set in Python

要缩小分形部分,请更改该区域矩阵的边界,并将迭代频率增加到 10 或更多。还可以尝试 Matplotlib 提供的各种颜色图。但是,为了发挥我们的创造力,我们可能需要尝试使用 Pillow,它是 Python 最流行的图像库之一。

结论

我们现在知道如何使用 Python 绘制著名的本华·曼德尔布罗特分形。有许多方法可以使用颜色、灰度、黑白和其他可视化方式来可视化它。已展示一个实际示例,以说明 Python 如何优雅地表达复杂的数学公式。

在本教程中,我们学习了如何

  • 解决一个实际问题,复数可以应用。
  • 找到曼德尔布罗集或朱利亚集的成员。
  • 这些集合可以作为分形与 Matplotlib 一起绘制。

下一主题Python Dbm 模块