大家好,我是你们的老朋友qmwneb946!

今天,我们要踏上一段奇妙的旅程,去探索一个既抽象又极其迷人的数学概念——分形维数。想象一下,一片云朵、一棵树、一段海岸线,它们看似杂乱无章,却又有着某种内在的规律。传统几何学中的“点”、“线”、“面”只能粗略地描述它们,但分形几何学却能用一个非整数的“维数”来精确捕捉它们的复杂程度和占据空间的能力。这听起来是不是很有趣?

分形维数不仅仅是数学家的玩具,它在物理学、生物学、医学、计算机图形学、金融市场分析等领域都有着广泛的应用。理解并计算分形维数,就像是为我们打开了一扇窗,让我们能够从一个全新的视角去理解自然界乃至我们身边无处不在的复杂现象。

在本篇博客中,我将带领大家深入浅出地理解分形维数的概念,并详细介绍几种最常用、最实用的分形维数计算方法,包括它们的原理、计算步骤、适用场景,并附上清晰的Python代码示例。无论你是数学爱好者,还是对数据分析、复杂系统感兴趣的开发者,相信这篇文章都能为你带来新的启发。

准备好了吗?让我们一起走进分形维数的世界!

什么是分形?超越传统的几何边界

在我们的传统认知中,几何图形有着明确的整数维数:点是0维,线是1维,平面是2维,立体是3维。然而,大自然中的许多现象却无法用这种简单的整数维数来描述。例如,一棵树的枝丫、一片云的轮廓、人体的血管网络,它们都介于“线”和“面”之间,或者介于“面”和“体”之间。

1975年,本华·曼德布罗特(Benoit Mandelbrot)提出了“分形”(Fractal)这个概念。分形是一种具有以下特征的几何形状:

  • 自相似性(Self-similarity):无论你放大多少倍,分形在不同尺度下都呈现出相似的结构。就像一棵树,你放大它的树枝,会发现树枝的结构和整棵树的结构相似。这种自相似可以是严格的(如科赫雪花),也可以是统计意义上的(如海岸线)。
  • 无限细节(Infinite Detail):分形在任何尺度下都包含着无限的细节。你无法找到一个“光滑”的部分,总是可以发现更小的结构。
  • 非整数维数(Non-integer Dimension):这是分形最核心的特征。它的维数通常是一个非整数,它比其拓扑维数(我们通常理解的整数维数)要高。

一些经典的分形例子包括:

  • 科赫雪花(Koch Snowflake):通过不断将线段中间三分之一替换为一个等边三角形来生成。它具有无限长的周长,但占据有限的面积。
  • 谢尔宾斯基垫片(Sierpinski Gasket):通过不断移除中心三角形来生成。
  • 曼德博集合(Mandelbrot Set):通过迭代复数方程 zn+1=zn2+cz_{n+1} = z_n^2 + c 生成的,具有极其复杂的边界。

分形的出现,极大地拓展了我们对几何学和自然复杂性的理解,为描述那些传统欧几里得几何无法捕捉的“粗糙”或“破碎”的形状提供了有力的工具。

为什么需要分形维数?

正如前面提到的,传统拓扑维数在描述许多自然现象和复杂系统时显得力不从心。拓扑维数只能告诉我们一个对象是线、面还是体,但无法量化其内部的复杂程度或“填充空间”的能力。

分形维数正是为了解决这个问题而生。它不是一个简单的整数,而是一个可以衡量分形在不同尺度下复杂性、不规则性和空间填充能力的度量。

  • 直观理解:一个分形维数越接近其嵌入空间的维数,就说明它越“密实”,越能填充空间。例如,一条平直的线是1维的。如果这条线变得非常弯曲、扭曲,甚至自身折叠,它的分形维数可能会大于1,但小于2。它开始占据更多的“平面”空间,但仍然不是一个真正的平面。

  • 应用价值

    • 自然科学:描述海岸线的复杂性、树木和血管的分支结构、闪电的路径、云团的形状、地貌的起伏等。
    • 医学:分析肿瘤边界的复杂性、心电图(ECG)信号的变异性、脑电图(EEG)的复杂性,以辅助疾病诊断。
    • 金融市场:研究股票价格波动的复杂性和自相似性,预测市场走势。
    • 图像处理与计算机图形学:用于纹理分析、图像压缩、图像合成(如生成逼真的山脉、火焰等)。
    • 材料科学:描述多孔材料、碎裂表面的结构。

分形维数提供了一个量化的指标,让我们能够比较不同复杂系统的“粗糙度”或“复杂性”,从而揭示其背后的规律。

分形维数:理论基石

在深入探讨具体的计算方法之前,我们先了解几种理论上重要的分形维数概念。

拓扑维数(Topological Dimension)

这是我们最熟悉的维数概念。它是一个整数,描述了对象的“连通性”和“自由度”。

  • 点:0维
  • 线段:1维
  • 平面区域:2维
  • 立体区域:3维

拓扑维数是分形维数的下限。一个分形,其分形维数总是大于或等于其拓扑维数。

相似维数(Similarity Dimension)

相似维数适用于具有严格自相似性的分形。这意味着分形的每一个小部分都是整个分形的精确缩小版本。
考虑一个由 NN 个缩小了 1/r1/r 倍的自身副本组成的分形。其相似维数 DsD_s 定义为:

Ds=logNlogrD_s = \frac{\log N}{\log r}

这里的 NN 是将一个大分形分解成小分形副本的数量,rr 是放大因子(或者 1/r1/r 是缩小因子)。

例子:科赫雪花(Koch Snowflake)
一个科赫曲线段是由4个缩小为1/3的自身副本组成的。
初始线段长度为 L0L_0。将其三等分,中间段替换为两个边长为 L0/3L_0/3 的线段组成的等边三角形。
这样,一个原始线段被替换为4个长度为 L0/3L_0/3 的小线段。
所以,N=4N = 4r=3r = 3(因为每个小线段的长度是原始线段的 1/31/3)。
科赫雪花的相似维数:

Ds=log4log31.2618D_s = \frac{\log 4}{\log 3} \approx 1.2618

这告诉我们,科赫雪花比一条线(1维)更复杂,但又不像一个面(2维)那样能够完全填充空间。

例子:谢尔宾斯基三角形(Sierpinski Triangle)
一个谢尔宾斯基三角形是由3个缩小为1/2的自身副本组成的。
所以,N=3N = 3r=2r = 2
谢尔宾斯基三角形的相似维数:

Ds=log3log21.585D_s = \frac{\log 3}{\log 2} \approx 1.585

代码示例(概念性):计算严格自相似分形的相似维数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import math

def calculate_similarity_dimension(N, r):
"""
计算严格自相似分形的相似维数。

参数:
N (int): 将分形分解成的小副本数量。
r (int/float): 每个小副本相对于原始分形的缩放因子 (1/r 是缩小的倍数,r是放大的倍数)。

返回:
float: 分形的相似维数。
"""
if N <= 0 or r <= 1:
raise ValueError("N 必须大于0,r 必须大于1。")
return math.log(N) / math.log(r)

# 示例:科赫雪花
N_koch = 4
r_koch = 3
dim_koch = calculate_similarity_dimension(N_koch, r_koch)
print(f"科赫雪花的相似维数: {dim_koch:.4f}")

# 示例:谢尔宾斯基三角形
N_sierpinski = 3
r_sierpinski = 2
dim_sierpinski = calculate_similarity_dimension(N_sierpinski, r_sierpinski)
print(f"谢尔宾斯基三角形的相似维数: {dim_sierpinski:.4f}")

# 示例:康托尔集 (Cantor Set)
# 康托尔集是将线段三等分,移除中间段,然后对剩下两段重复操作。
# 一个线段变为2个1/3长度的副本。
N_cantor = 2
r_cantor = 3
dim_cantor = calculate_similarity_dimension(N_cantor, r_cantor)
print(f"康托尔集的相似维数: {dim_cantor:.4f}")

优点与缺点:

  • 优点:概念直观,计算简单,对严格自相似分形非常精确。
  • 缺点:只适用于严格自相似分形。对于自然界中大多数只有统计自相似性的分形(如海岸线、云朵),或者由迭代函数系统(IFS)生成的分形,相似维数不再适用。

豪斯多夫维数(Hausdorff Dimension)

豪斯多夫维数是分形维数最严格的数学定义,也是所有分形维数的“黄金标准”。它是一个拓扑不变量,能够精确地量化一个集合的“大小”或“复杂性”,即使这个集合在拓扑维数上是0维(如康托尔集)。

豪斯多夫维数的定义非常复杂,涉及到集合的“覆盖”和“测度”。简单来说,它通过用直径趋近于零的球体(或集合)来覆盖一个集合,并计算这些球体直径的某个幂次之和的最小值。当这个幂次取某个特定值 DHD_H 时,这个和会在一个临界点从无限大变为零。这个临界值就是豪斯多夫维数。

DH=inf{s0:Hs(X)=0} if Hs(X)<D_H = \inf \{ s \ge 0 : \mathcal{H}^s(X) = 0 \} \text{ if } \mathcal{H}^s(X) < \infty

DH=sup{s0:Hs(X)=} if Hs(X)>0D_H = \sup \{ s \ge 0 : \mathcal{H}^s(X) = \infty \} \text{ if } \mathcal{H}^s(X) > 0

其中 Hs(X)\mathcal{H}^s(X)XXss 维豪斯多夫测度。

优点:最普适和严格的定义,能应用于任何集合。
缺点:理论性强,计算极其复杂,几乎无法直接通过实验数据进行计算。在实践中,我们通常使用近似方法,如盒计数维数或关联维数。

盒计数维数(Box-Counting Dimension)

盒计数维数(也称为闵可夫斯基-布里赫姆维数或网格维数)是计算分形维数最常用且最实用的方法。它适用于任何图像或点集,甚至是那些不具有严格自相似性的对象。

原理

盒计数维数的原理是:用边长为 ϵ\epsilon 的小盒子(或网格)去覆盖一个分形集合。统计覆盖该集合所需的最小盒子数量 N(ϵ)N(\epsilon)
对于一个分形,当 ϵ\epsilon 趋近于0时, N(ϵ)N(\epsilon)ϵ\epsilon 之间存在一个幂律关系:

N(ϵ)ϵDboxN(\epsilon) \propto \epsilon^{-D_{box}}

取对数后,我们可以得到一个线性关系:

logN(ϵ)Dboxlogϵ\log N(\epsilon) \propto -D_{box} \log \epsilon

或者:

Dbox=limϵ0logN(ϵ)log(1/ϵ)D_{box} = \lim_{\epsilon \to 0} \frac{\log N(\epsilon)}{\log (1/\epsilon)}

通过在不同 ϵ\epsilon 值下计算 N(ϵ)N(\epsilon),然后绘制 logN(ϵ)\log N(\epsilon)log(1/ϵ)\log (1/\epsilon) 的散点图(称为双对数图),这条直线的斜率就是盒计数维数 DboxD_{box}

算法步骤

  1. 准备数据:将分形对象表示为二值图像(黑色像素代表分形,白色像素代表背景)或一个点集。
  2. 选择尺度范围:选择一系列不同的盒子边长 ϵi\epsilon_i。通常从图像尺寸的1/2开始,逐步减小到几个像素。
  3. 遍历尺度:对于每一个 ϵi\epsilon_i
    • 将整个图像(或包含点集的区域)分割成边长为 ϵi×ϵi\epsilon_i \times \epsilon_i 的网格。
    • 统计包含至少一个分形像素(或点)的盒子数量 N(ϵi)N(\epsilon_i)
  4. 数据拟合:将所有 (log(1/ϵi),logN(ϵi))(\log (1/\epsilon_i), \log N(\epsilon_i)) 的数据点绘制在双对数图上。
  5. 线性回归:对这些点进行线性回归,拟合出一条直线。这条直线的斜率就是盒计数维数。

Python 代码示例:计算二值图像的盒计数维数

我们将使用numpy进行数值计算,matplotlib进行绘图,scipy.stats进行线性回归。

首先,我们需要一个示例分形图像。我们可以生成一个简单的谢尔宾斯基三角形作为测试用例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import linregress
import skimage.io
import skimage.transform
from PIL import Image

# --- 1. 生成谢尔宾斯基三角形 (作为测试图像) ---
def sierpinski_gasket(order, size):
"""生成一个谢尔宾斯基三角形的二值图像。"""
img = np.zeros((size, size), dtype=np.uint8)

def draw_triangle(p1, p2, p3, current_order):
if current_order == 0:
# 绘制最小的三角形
coords = np.array([p1, p2, p3], dtype=np.int32)
# 使用 PIL.ImageDraw 来填充多边形
temp_img = Image.new('L', (size, size), 0)
from PIL import ImageDraw
draw = ImageDraw.Draw(temp_img)
draw.polygon([tuple(c) for c in coords], fill=255)
img[temp_img == 255] = 255
return

mid12 = ((p1[0] + p2[0]) // 2, (p1[1] + p2[1]) // 2)
mid23 = ((p2[0] + p3[0]) // 2, (p2[1] + p3[1]) // 2)
mid31 = ((p3[0] + p1[0]) // 2, (p3[1] + p1[1]) // 2)

draw_triangle(p1, mid12, mid31, current_order - 1)
draw_triangle(mid12, p2, mid23, current_order - 1)
draw_triangle(mid31, mid23, p3, current_order - 1)

# 初始大三角形的顶点 (假设图像是正方形)
p_top = (size // 2, 0)
p_left = (0, size - 1)
p_right = (size - 1, size - 1)

draw_triangle(p_top, p_left, p_right, order)
return img

# 生成一个256x256的谢尔宾斯基三角形
# 为了简化,这里我们不画外部三角形,只关注内部分形结构
# 使用一个更简单的方式来生成谢尔宾斯基垫片(反向填充)
def make_sierpinski_gasket_mask(n, size):
img = np.zeros((size, size), dtype=np.uint8)
# 填充整个图像为1 (代表前景)
img.fill(1)

def remove_middle(x1, y1, x2, y2, order):
if order == 0:
return

# 找到中心三角形的顶点
mid_x1 = (2 * x1 + x2) // 3
mid_y1 = (2 * y1 + y2) // 3
mid_x2 = (x1 + 2 * x2) // 3
mid_y2 = (y1 + 2 * y2) // 3

# 移除中间的九宫格
img[mid_y1:mid_y2, mid_x1:mid_x2] = 0

# 对剩下的8个子正方形递归
# 0,0 0,1 0,2
# 1,0 1,1 1,2
# 2,0 2,1 2,2
# 我们移除的是 (1,1) 区域

# Top row
remove_middle(x1, y1, mid_x1, mid_y1, order - 1)
remove_middle(mid_x1, y1, mid_x2, mid_y1, order - 1)
remove_middle(mid_x2, y1, x2, mid_y1, order - 1)
# Middle row (excluding center)
remove_middle(x1, mid_y1, mid_x1, mid_y2, order - 1)
remove_middle(mid_x2, mid_y1, x2, mid_y2, order - 1)
# Bottom row
remove_middle(x1, mid_y2, mid_x1, y2, order - 1)
remove_middle(mid_x1, mid_y2, mid_x2, y2, order - 1)
remove_middle(mid_x2, mid_y2, x2, y2, order - 1)

remove_middle(0, 0, size, size, n)
return img * 255 # 转换为0-255灰度图

sierpinski_image_size = 512
sierpinski_gasket_order = 4
sierpinski_img = make_sierpinski_gasket_mask(sierpinski_gasket_order, sierpinski_image_size)

plt.figure(figsize=(6, 6))
plt.imshow(sierpinski_img, cmap='gray')
plt.title(f'Sierpinski Gasket (Order {sierpinski_gasket_order}, {sierpinski_image_size}x{sierpinski_image_size})')
plt.axis('off')
plt.show()

# --- 2. 盒计数维数计算函数 ---
def box_counting_dimension(image_array, scales=None):
"""
计算二值图像的盒计数维数。

参数:
image_array (np.array): 输入的二值图像 (0为背景,非0为分形)。
scales (list/np.array, optional): 要使用的盒子边长列表。如果为None,
将自动生成一个合适的范围。

返回:
tuple: (dimension, log_scales, log_counts)
dimension (float): 计算出的盒计数维数。
log_scales (np.array): log(1/epsilon) 值。
log_counts (np.array): log(N(epsilon)) 值。
"""
if image_array.ndim > 2:
# 如果是彩色图像,转换为灰度并二值化
image_array = np.mean(image_array, axis=-1)

# 将图像二值化 (0或1)
binary_image = (image_array > 0).astype(int)

rows, cols = binary_image.shape

if scales is None:
# 自动生成一系列尺度(盒子边长)
# 通常从最小边长开始,直到图像尺寸的某个比例
min_dim = min(rows, cols)
# 生成2的幂次作为尺度,从2^1 到 2^k,其中 2^k <= min_dim / 2 (留出足够的点用于回归)
# 或者从 2 到 min_dim // 2

# 经验性地选择尺度范围,确保有足够多的点且不会太小导致噪声
scales = [2**i for i in range(1, int(np.log2(min_dim)))]
# 过滤掉过小的尺度 (例如小于2x2的盒子) 和过大的尺度 (例如大于图像一半的盒子)
scales = [s for s in scales if s >= 2 and s <= min_dim / 2]


n_counts = []

for epsilon in scales:
count = 0
# 遍历图像,以epsilon为步长进行网格划分
for r_start in range(0, rows, epsilon):
for c_start in range(0, cols, epsilon):
# 检查当前盒子内是否包含分形像素
box = binary_image[r_start : r_start + epsilon,
c_start : c_start + epsilon]
if np.sum(box) > 0: # 如果盒子里有前景像素
count += 1
n_counts.append(count)

# 将epsilon和N(epsilon)转换为对数形式
log_epsilon_inv = np.log(1.0 / np.array(scales))
log_n_counts = np.log(np.array(n_counts))

# 线性回归拟合
# D_box = slope of log(N(epsilon)) vs log(1/epsilon)
slope, intercept, r_value, p_value, std_err = linregress(log_epsilon_inv, log_n_counts)

return slope, log_epsilon_inv, log_n_counts

# 执行计算
# 对于谢尔宾斯基垫片,理论盒计数维数是 log(8)/log(3) ~= 1.89
# 我们的生成方式是类似康托尔地毯,理论值是 log(8)/log(3)
# (因为每次将一个正方形分成9个小正方形,移除中间一个,剩下8个)
# 所以理论值是 log(8)/log(3) = 3 * log(2) / log(3) approx 1.8927
dimension, log_scales, log_counts = box_counting_dimension(sierpinski_img)

print(f"计算出的盒计数维数: {dimension:.4f}")

# 绘制双对数图
plt.figure(figsize=(8, 6))
plt.scatter(log_scales, log_counts, label='数据点', color='blue')
plt.plot(log_scales, dimension * log_scales + (log_counts[0] - dimension * log_scales[0]),
label=f'拟合直线 (D={dimension:.4f})', color='red', linestyle='--')
plt.xlabel('$\log(1/\epsilon)$')
plt.ylabel('$\log(N(\epsilon))$')
plt.title('盒计数维数双对数图')
plt.legend()
plt.grid(True)
plt.show()

# 示例:加载一个外部图片并计算
# try:
# # 假设你有一个名为 'fractal_image.png' 的图片
# external_image = skimage.io.imread('fractal_image.png', as_gray=True)
# external_image = (external_image > np.mean(external_image)).astype(np.uint8) * 255 # 简单二值化
#
# plt.figure(figsize=(6,6))
# plt.imshow(external_image, cmap='gray')
# plt.title('External Fractal Image')
# plt.axis('off')
# plt.show()
#
# dim_ext, ls_ext, lc_ext = box_counting_dimension(external_image)
# print(f"外部图像的盒计数维数: {dim_ext:.4f}")
#
# plt.figure(figsize=(8, 6))
# plt.scatter(ls_ext, lc_ext, label='数据点', color='blue')
# plt.plot(ls_ext, dim_ext * ls_ext + (lc_ext[0] - dim_ext * ls_ext[0]),
# label=f'拟合直线 (D={dim_ext:.4f})', color='red', linestyle='--')
# plt.xlabel('$\log(1/\epsilon)$')
# plt.ylabel('$\log(N(\epsilon))$')
# plt.title('外部图像盒计数维数双对数图')
# plt.legend()
# plt.grid(True)
# plt.show()
#
# except FileNotFoundError:
# print("未找到 'fractal_image.png'。请确保文件存在或生成一个。")

盒计数维数的优点与缺点

优点

  • 普适性:适用于各种类型的分形,无论是几何分形还是非规则的自然分形。
  • 实现简单:算法直观,易于编程实现。
  • 计算效率相对较高:对于图像数据,可以有效地计算。

缺点

  • 对图像分辨率敏感:图像的分辨率限制了可用的最小 ϵ\epsilon 值,从而影响计算精度。
  • 边缘效应:在图像边缘,盒子可能无法完全覆盖分形,导致计数误差。
  • 尺度范围的选择ϵ\epsilon 的选择范围很重要。过小的 ϵ\epsilon 可能会受到图像像素化和噪声的影响;过大的 ϵ\epsilon 则无法捕捉分形的精细结构。通常需要在一个合适的“标度不变”区间内进行线性拟合。
  • 噪声敏感:图像中的噪声点可能被误识别为分形的一部分,从而影响 N(ϵ)N(\epsilon) 的计数。

关联维数(Correlation Dimension)

关联维数是另一种常用的分形维数,尤其适用于点集数据,例如从混沌系统(如洛伦兹吸引子)中采样得到的时间序列嵌入后的相空间点集。它衡量了点集内部点的聚集程度。

原理

关联维数 DcD_c 基于“关联积分” C(r)C(r) 的概念。关联积分 C(r)C(r) 定义为在给定点集中,任意两个点之间的距离小于 rr 的点对的比例。

对于一个包含 MM 个点的集合 {x1,x2,,xM}\{x_1, x_2, \dots, x_M\},关联积分 C(r)C(r) 定义为:

C(r)=limM1M2ijH(rxixj)C(r) = \lim_{M \to \infty} \frac{1}{M^2} \sum_{i \ne j} H(r - |x_i - x_j|)

其中 H()H(\cdot) 是赫维赛德阶跃函数(Heaviside step function):

H(x)={1if x00if x<0H(x) = \begin{cases} 1 & \text{if } x \ge 0 \\ 0 & \text{if } x < 0 \end{cases}

rr 趋近于0时,关联积分 C(r)C(r)rr 之间也存在一个幂律关系:

C(r)rDcC(r) \propto r^{D_c}

取对数后,我们可以得到一个线性关系:

logC(r)Dclogr\log C(r) \propto D_c \log r

因此,关联维数 DcD_c 可以通过绘制 logC(r)\log C(r)logr\log r 的双对数图,并计算直线的斜率来得到:

Dc=limr0logC(r)logrD_c = \lim_{r \to 0} \frac{\log C(r)}{\log r}

算法步骤

  1. 准备数据:一个 M×dM \times d 的矩阵,其中 MM 是点数,dd 是点的维度。
  2. 选择距离范围:选择一系列不同的半径 rkr_k。通常从点集的最大距离(或包含点集的超立方体对角线长度)的一个比例开始,逐步减小到最小非零距离。
  3. 计算距离矩阵:计算点集中所有点对之间的欧几里得距离。
  4. 遍历半径:对于每一个 rkr_k
    • 统计有多少对点的距离小于 rkr_k
    • 计算 C(rk)=(满足条件的点对数量)/M2C(r_k) = (\text{满足条件的点对数量}) / M^2
  5. 数据拟合:将所有 (logrk,logC(rk))(\log r_k, \log C(r_k)) 的数据点绘制在双对数图上。
  6. 线性回归:对这些点进行线性回归,拟合出一条直线。这条直线的斜率就是关联维数 DcD_c

Python 代码示例:计算点集的关联维数

我们将生成一个简单的二维随机点集(或者更复杂一点,例如洛伦兹吸引子的点集,但为了代码简洁和通用性,这里先用随机点模拟)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import numpy as np
import matplotlib.pyplot as plt
from scipy.spatial.distance import pdist, squareform
from scipy.stats import linregress

# --- 1. 生成示例点集 ---
# 这里我们生成一个简单的随机二维点集,或者你可以替换为混沌吸引子的数据
def generate_lorenz_attractor(num_points=10000, sigma=10, rho=28, beta=8/3):
"""
生成洛伦兹吸引子的点集。
"""
dt = 0.01
xs, ys, zs = [0.1], [0.1], [0.1]

for _ in range(num_points - 1):
x_dot = sigma * (ys[-1] - xs[-1])
y_dot = xs[-1] * (rho - zs[-1]) - ys[-1]
z_dot = xs[-1] * ys[-1] - beta * zs[-1]

xs.append(xs[-1] + x_dot * dt)
ys.append(ys[-1] + y_dot * dt)
zs.append(zs[-1] + z_dot * dt)

return np.array([xs, ys, zs]).T

# 生成洛伦兹吸引子点集
num_points = 20000 # 增加点数以获得更好的统计效果
lorenz_points = generate_lorenz_attractor(num_points=num_points)

# 绘制洛伦兹吸引子 (2D投影)
plt.figure(figsize=(8, 6))
plt.scatter(lorenz_points[:, 0], lorenz_points[:, 2], s=0.1, alpha=0.5, color='blue')
plt.title('Lorenz Attractor (X-Z Projection)')
plt.xlabel('X')
plt.ylabel('Z')
plt.grid(True)
plt.show()

# --- 2. 关联维数计算函数 ---
def correlation_dimension(points, num_bins=50, max_dist_ratio=0.5):
"""
计算点集的关联维数。

参数:
points (np.array): NxM 维的 NumPy 数组,N 是点数,M 是维度。
num_bins (int): 用于计算距离分布的直方图或 r 值的数量。
max_dist_ratio (float): 用于设置最大距离的比例,相对于点对最大距离。

返回:
tuple: (dimension, log_r, log_Cr)
dimension (float): 计算出的关联维数。
log_r (np.array): log(r) 值。
log_Cr (np.array): log(C(r)) 值。
"""
# 1. 计算所有点对之间的欧几里得距离
# pdist 计算所有点对之间的距离,返回扁平数组
# squareform 将扁平数组转换为方阵 (距离矩阵)
distances = pdist(points)

# 2. 确定距离范围
# 最小非零距离和最大距离
min_nonzero_dist = np.min(distances[distances > 0]) # 避免 log(0)
max_dist = np.max(distances)

# 根据 max_dist_ratio 调整最大距离,避免噪声和饱和效应
r_values = np.logspace(np.log10(min_nonzero_dist), np.log10(max_dist * max_dist_ratio), num_bins)

Cr_values = []

# 3. 遍历每个r值,计算关联积分 C(r)
for r in r_values:
# 统计距离小于r的点对数量
# 这里考虑 i != j,所以我们不计算点与自身的距离 (距离为0)
# distances 是一个 M*(M-1)/2 的扁平数组,直接统计即可
num_pairs_less_than_r = np.sum(distances < r)

# 关联积分 C(r) = (点对数量) / (总点对数量 M*(M-1)/2)
# 注意:这里 Pdist 返回的是 N*(N-1)/2 对距离,所以总对数应该是这个值,而不是 M^2
# 严格按照定义 C(r) = (1/M^2) * sum(H(r-|xi-xj|))
# 等价于 (2 / (M * (M-1))) * (sum(distances < r)) [for i != j]
# 或者简化为 (num_pairs_less_than_r / (M*(M-1)/2))
# C(r) 的定义可以有多种,这里我们使用相对更常用的形式,即总点对数归一化
total_pairs = len(points) * (len(points) - 1) / 2 # for i!=j
Cr = num_pairs_less_than_r / total_pairs if total_pairs > 0 else 0

Cr_values.append(Cr)

log_r = np.log10(r_values)
log_Cr = np.log10(np.array(Cr_values))

# 过滤掉 log_Cr 中为 -inf 的值 (C(r) = 0)
finite_indices = np.isfinite(log_Cr) & np.isfinite(log_r)
log_r_filtered = log_r[finite_indices]
log_Cr_filtered = log_Cr[finite_indices]

# 确保至少有两个点进行回归
if len(log_r_filtered) < 2:
return 0, log_r, log_Cr # 无法计算

# 线性回归拟合
# D_c = slope of log(C(r)) vs log(r)
slope, intercept, r_value, p_value, std_err = linregress(log_r_filtered, log_Cr_filtered)

return slope, log_r_filtered, log_Cr_filtered

# 执行计算
# 洛伦兹吸引子的关联维数理论值约为 2.06
dimension_corr, log_r, log_Cr = correlation_dimension(lorenz_points, num_bins=100, max_dist_ratio=0.3)

print(f"计算出的关联维数: {dimension_corr:.4f}")

# 绘制双对数图
plt.figure(figsize=(8, 6))
plt.scatter(log_r, log_Cr, label='数据点', color='blue')
# 绘制拟合直线
plt.plot(log_r, dimension_corr * log_r + (log_Cr[0] - dimension_corr * log_r[0]),
label=f'拟合直线 (D={dimension_corr:.4f})', color='red', linestyle='--')
plt.xlabel('$\log(r)$')
plt.ylabel('$\log(C(r))$')
plt.title('关联维数双对数图')
plt.legend()
plt.grid(True)
plt.show()

关联维数的优点与缺点

优点

  • 适用于点集数据:特别适合分析从动态系统生成的时间序列数据。
  • 计算相对稳定:对噪声的敏感度通常低于盒计数维数,因为它基于统计而不是精确覆盖。
  • 提供更多信息:关联积分本身可以提供关于数据分布的额外信息。

缺点

  • 计算复杂性:需要计算所有点对之间的距离,对于大规模点集(例如 M>104M > 10^4)计算复杂度为 O(M2)O(M^2),计算量非常大。
  • 对数据量要求高:需要足够多的数据点才能得到稳定的结果。
  • rr 范围的选择:与盒计数类似,rr 的选择范围(以及要进行线性拟合的区域)至关重要。通常在中间区域(“标度不变区”)进行拟合,因为太小的 rr 受噪声影响,太大的 rr 则失去了分形特性。

信息维数(Information Dimension)

信息维数 DID_I 是广义分形维数的一种,它介于关联维数和盒计数维数之间。它在盒计数维数的基础上,考虑了每个盒子中点(或像素)分布的概率信息,用香农熵来衡量。

原理

与盒计数维数一样,信息维数也使用边长为 ϵ\epsilon 的盒子来覆盖分形。但它不仅仅统计非空盒子的数量,而是统计每个盒子 ii 中包含的分形元素的比例 pip_i

然后,计算这些盒子的信息熵 I(ϵ)I(\epsilon)

I(ϵ)=ipilogpiI(\epsilon) = -\sum_i p_i \log p_i

其中 pip_i 是第 ii 个盒子中分形元素的概率(例如,该盒子中点数占总点数的比例)。

信息维数 DID_I 定义为:

DI=limϵ0I(ϵ)log(1/ϵ)D_I = \lim_{\epsilon \to 0} \frac{I(\epsilon)}{\log (1/\epsilon)}

同样,通过绘制 log(1/ϵ)\log (1/\epsilon)I(ϵ)I(\epsilon) 的双对数图,其斜率即为信息维数。

何时使用

信息维数比盒计数维数更精细,因为它考虑了分形在空间中的不均匀分布。如果分形在某些区域比其他区域更密集,信息维数会更好地捕捉这种特性。它对于分析具有非均匀密度的吸引子特别有用。

优点:比盒计数维数更敏感,能区分密度不均匀的分形。
缺点:计算比盒计数复杂,需要额外计算每个盒子的概率。

其他维数概念和方法(简要提及)

除了上述几种常用方法外,分形几何中还有其他一些维数概念或相关度量:

谱维数(Spectral Dimension)

谱维数与分形上的扩散过程(如随机游走)有关,它描述了分形上扩散过程的有效维数。例如,在一个分形上随机游走的粒子,其回到原点的概率衰减速度与谱维数有关。它在物理学中用于描述无序材料的性质。

填函维数(Lacunarity)

填函维数(或空隙率)不是一个严格意义上的分形维数,但它是描述分形纹理的另一个重要参数。它量化了分形的“孔洞大小”或“空隙分布”的均匀性。即使两个分形具有相同的分形维数,它们的填函维数也可能不同,这使得填函维数在区分具有相同维数但视觉上不同的分形时非常有用。

实际应用中的考虑

在实际计算分形维数时,我们面临的往往是有限、离散且可能带有噪声的数据。这使得理论上的极限计算变得复杂,需要我们进行一些实际的权衡和处理:

  • 有限数据的影响:无论是图像像素还是点集,我们能获得的数据总是有限的。这意味着 ϵ0\epsilon \to 0r0r \to 0 的极限无法真正达到。我们只能在有限的尺度范围内进行近似。
  • 噪声的影响:真实数据中不可避免地存在噪声。在非常小的尺度下,噪声可能被误识别为分形结构,导致维数估计偏高。
  • 尺度范围的选择(“标度不变区”):在双对数图中,通常只有在某个中间尺度范围(即所谓的“标度不变区”或“线性区域”)内,数据点才呈现出良好的线性关系。过小或过大的尺度都可能偏离这条直线。正确识别并选择这个线性区域是获得准确维数估值的关键。这通常需要通过观察双对数图来手动选择,或使用一些自动算法。
  • 拟合方法的选择:最常用的方法是最小二乘线性回归。但在数据点偏离线性关系较多时,可能需要更鲁棒的拟合方法。
  • 数据预处理:对于图像数据,合适的二值化、去噪和边缘检测可能对结果有显著影响。对于点集数据,去除异常值和规范化也很重要。

结论

分形维数是一个强大而迷人的数学工具,它为我们理解和量化复杂性提供了一个独特的视角。从曼德布罗特的海岸线到人体内的血管网络,分形无处不在,而分形维数正是描述这些复杂结构的核心指标。

我们深入探讨了几种主要的计算方法:

  • 相似维数:直观简洁,适用于严格自相似分形。
  • 盒计数维数:最常用、最普适的方法,适用于图像和点集,通过网格覆盖统计非空盒子数量。
  • 关联维数:尤其适用于点集,特别是混沌吸引子,通过点对距离的统计分布来计算。
  • 信息维数:在盒计数基础上考虑了空间概率分布,适用于密度不均匀的分形。

每种方法都有其特定的适用场景、优缺点和计算考量。在实际应用中,选择合适的方法,并对数据进行恰当的预处理和尺度范围选择,是获得可靠分形维数值的关键。

分形几何和分形维数的研究仍在不断深入,它将继续在各个科学领域发挥重要作用。希望这篇博客能为你打开一扇窗,让你对这个充满奇妙和实用价值的数学世界有更深刻的理解。

我是qmwneb946,感谢你的阅读!如果你对分形维数有任何疑问,或者有其他有趣的数学和技术问题想和我交流,欢迎在评论区留言。我们下期再见!