
本教程详细介绍了如何利用numpy的`np.lib.stride_tricks.as_strided`功能,结合查找表(lut)高效地对二维数组进行2x2块的修改。通过将原始数组转换为一个由2x2子块组成的视图,并利用numpy的矢量化索引能力,我们可以避免低效的python循环,从而显著提升处理大规模数组时的性能。文章还探讨了通过单索引查找表进一步优化操作的方法,并提供了详细的代码示例和使用注意事项。
在处理大型二维数组时,如果需要对固定大小的子块(例如2x2)进行遍历和修改,传统的Python循环结合切片操作往往效率低下。NumPy作为Python科学计算的核心库,提供了强大的矢量化能力,可以显著优化这类操作。本文将深入探讨如何利用np.lib.stride_tricks.as_strided和查找表(Lookup Table, LUT)来实现对2D数组2x2块的高效修改。
考虑以下场景:您有一个二维网格,需要遍历其中所有不重叠的2x2子块,并根据每个子块的当前值从一个预定义的映射(transitions)中获取新的值来更新该子块。原始问题中给出的Python循环示例如下:
import numpy as np
import itertools
# 假设 transitions 是一个映射,将2x2块的字节表示映射到新的2x2布尔数组
# transitions = {b'\x00\x00\x00\x00': np.array([True, True, True, True]), …}
# 假设 grid 是一个二维NumPy数组
# ny, nx = grid.shape
# yshift, xshift = 0, 0 # 示例,实际可能根据需求调整
# for y, x in itertools.product(range(yshift, ny, 2), range(xshift, nx, 2)):
# block = grid[y:y+2, x:x+2]
# grid[y:y+2, x:x+2].flat = transitions[block.tobytes()]这种方法虽然直观,但由于涉及到大量的Python级别循环和切片操作,对于大型数组而言,其性能瓶颈非常明显。NumPy的优势在于其底层C/Fortran实现,能够对整个数组或其视图进行批量操作,从而避免了Python解释器的开销。
NumPy的np.lib.stride_tricks.as_strided函数是一个强大但需要谨慎使用的工具。它允许我们创建一个现有数组的“视图”,而无需复制数据。通过巧妙地设置视图的shape和strides,我们可以将一个二维数组看作是由多个重叠或不重叠的子块组成的更高维数组。
as_strided函数的核心在于shape和strides参数:
对于一个形状为 (N, M) 的二维数组 A,其步幅通常是 (A.itemsize * M, A.itemsize)。这意味着在行方向上移动一个元素需要跳过 M 个元素的大小,在列方向上移动一个元素需要跳过 1 个元素的大小。
要将一个 (10, 10) 的数组 A 视为一个 (5, 5) 的2x2块数组 Av,我们需要:
因此,Av 的步幅将是 (A.strides[0]*2, A.strides[1]*2, A.strides[0], A.strides[1])。
import numpy as np
# 示例:创建一个10x10的0/1随机数组
A = np.random.randint(0, 2, (10, 10))
print("原始数组 A:\n", A)
# 使用 as_strided 创建一个视图 Av
# shape=(5,5,2,2) 表示 Av 是一个 5x5 的数组,每个元素都是一个 2x2 的子数组
# strides=(A.strides[0]*2, A.strides[1]*2, A.strides[0], A.strides[1])
# 第一个2*A.strides[0]:在 Av 的第一个维度(块行)移动1,A 的行索引移动2
# 第二个2*A.strides[1]:在 Av 的第二个维度(块列)移动1,A 的列索引移动2
# 第三个A.strides[0]:在 Av 的第三个维度(块内行)移动1,A 的行索引移动1
# 第四个A.strides[1]:在 Av 的第四个维度(块内列)移动1,A 的列索引移动1
Av = np.lib.stride_tricks.as_strided(A, shape=(5, 5, 2, 2),
strides=(A.strides[0]*2, A.strides[1]*2,
A.strides[0], A.strides[1]))
print("\nAv 的形状:", Av.shape) # (5, 5, 2, 2)
print("Av 的步幅:", Av.strides)
print("A 的步幅:", A.strides)
# 验证 Av 是 A 的视图,修改 Av 会影响 A
# Av[0, 0, 0, 0] = 99
# print("\n修改 Av[0,0,0,0] 后 A 的左上角:\n", A[:2,:2])由于我们需要根据2x2块的当前值来决定其新的值,一个查找表是理想的选择。对于一个由0和1组成的2x2块,它有 $2^4 = 16$ 种可能的组合。我们可以创建一个多维数组作为查找表,其索引对应2x2块的四个元素。
# lut 的形状 (2,2,2,2,2,2) # 前四个 '2' 代表 2x2 块的四个布尔值 (0或1) # 后两个 '2,2' 代表替换后的 2x2 结果 lut = np.zeros((2, 2, 2, 2, 2, 2), dtype=A.dtype) # 填充查找表(根据您的 transitions 字典进行翻译) # 示例:如果原始块是 [[0,0],[0,0]],替换为 [[1,1],[1,1]] lut[0, 0, 0, 0] = [[1, 1], [1, 1]] # 示例:如果原始块是 [[0,0],[0,1]],替换为 [[1,1],[1,0]] lut[0, 0, 0, 1] = [[1, 1], [1, 0]] # 示例:如果原始块是 [[0,0],[1,0]],替换为 [[1,1],[0,1]] lut[0, 0, 1, 0] = [[1, 1], [0, 1]] # 示例:如果原始块是 [[1,1],[0,0]],替换为 [[1,1],[1,1]] lut[1, 1, 0, 0] = [[1, 1], [1, 1]] # 其他未定义的组合将保持为0(根据 lut 初始化)
有了 Av 视图和 lut,我们可以使用NumPy的“花式索引”(fancy indexing)来一次性更新所有2x2块。我们将 Av 中的每个2x2块的四个元素作为索引,去查找 lut 中对应的新值。
# 原始数组 A (再次生成一个,避免之前的修改影响)
A = np.random.randint(0, 2, (10, 10))
print("\n修改前的数组 A:\n", A)
# 重新创建 Av 视图
Av = np.lib.stride_tricks.as_strided(A, shape=(5, 5, 2, 2),
strides=(A.strides[0]*2, A.strides[1]*2,
A.strides[0], A.strides[1]))
# 使用花式索引和查找表更新所有 2x2 块
# Av[...,0,0] 提取所有 2x2 块的左上角元素
# Av[...,0,1] 提取所有 2x2 块的右上角元素
# Av[...,1,0] 提取所有 2x2 块的左下角元素
# Av[...,1,1] 提取所有 2x2 块的右下角元素
Av[:] = lut[Av[..., 0, 0], Av[..., 0, 1], Av[..., 1, 0], Av[..., 1, 1]]
print("\n修改后的数组 A:\n", A)通过这种方式,我们避免了显式的Python循环,将操作完全推送到NumPy的底层实现,从而实现了显著的性能提升。
假设我们有一个初始数组 A:
# 原始数组 A [[0 1 0 1 1 1 1 1 1 0] [0 1 1 1 1 1 1 1 0 0] [0 0 1 1 0 1 0 1 1 0] [0 0 0 1 0 1 1 0 1 1] [0 1 1 1 0 1 1 1 0 0] [0 0 0 0 0 1 1 0 0 0] [1 0 1 1 1 0 1 0 0 0] [0 1 1 0 1 0 0 0 1 0] [1 1 0 1 0 1 0 1 1 0] [0 1 1 1 1 1 1 1 1 1]]
经过上述 as_strided 和 lut 操作后,根据 lut 的定义,数组 A 将被修改为(部分示例):
# 修改后的数组 A [[0 0 0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0 0 0] [1 1 0 0 0 0 0 0 0 0] [1 1 0 0 0 0 0 0 0 0] [0 0 1 1 0 0 0 0 1 1] [0 0 1 1 0 0 0 0 1 1] [0 0 0 0 0 0 0 0 1 1] [0 0 0 0 0 0 0 0 0 1] [0 0 0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0 0 0]]
可以看到,原始数组的2x2子块根据 lut 中的规则被整体替换。
上述方法中,花式索引 lut[Av[..., 0, 0], Av[..., 0, 1], Av[..., 1, 0], Av[..., 1, 1]] 需要重复四次 Av 的索引,这在某些情况下可能显得冗长。我们可以通过将2x2块的四个布尔值(0或1)编码成一个0到15的单一整数索引来简化查找表的使用。
假设2x2块的元素为:
[[a, b], [c, d]]
我们可以将其转换为一个整数索引:idx = a*8 + b*4 + c*2 + d*1。
# 创建一个单索引的查找表
lut2 = np.zeros((16, 2, 2), dtype=A.dtype)
# 填充 lut2
# 示例:索引 0 (对应 [[0,0],[0,0]]) 替换为 [[1,1],[1,1]]
lut2[0] = [[1, 1], [1, 1]]
# 示例:索引 1 (对应 [[0,0],[0,1]]) 替换为 [[1,1],[1,0]]
lut2[1] = [[1, 1], [1, 0]]
# 示例:索引 2 (对应 [[0,0],[1,0]]) 替换为 [[1,1],[0,1]]
lut2[2] = [[1, 1], [0, 1]]
# 示例:索引 12 (对应 [[1,1],[0,0]]) 替换为 [[1,1],[1,1]]
lut2[12] = [[1, 1], [1, 1]]
# 其他未定义的组合将保持为0
# 原始数组 A (再次生成一个)
A = np.random.randint(0, 2, (10, 10))
print("\n使用单索引查找表前 A:\n", A)
# 重新创建 Av 视图
Av = np.lib.stride_tricks.as_strided(A, shape=(5, 5, 2, 2),
strides=(A.strides[0]*2, A.strides[1]*2,
A.strides[0], A.strides[1]))
# 将每个 2x2 块转换为一个 0-15 的整数索引
# Av*[[8,4],[2,1]] 会将每个 2x2 块的元素乘以对应的权重
# .sum(axis=(2,3)) 会对每个加权后的 2x2 块求和,得到一个 (5,5) 的索引数组
idx = (Av * [[8, 4], [2, 1]]).sum(axis=(2, 3))
# 使用单索引查找表更新所有 2x2 块
Av[:] = lut2[idx]
print("\n使用单索引查找表后 A:\n", A)这种方法通过一次计算得到所有块的索引,再进行一次查找,使得代码更加简洁。
通过巧妙地运用NumPy的np.lib.stride_tricks.as_strided函数创建数组的视图,并结合预先构建的查找表,我们可以实现对二维数组中2x2块的高效矢量化修改。这种方法避免了Python层面的循环开销,显著提升了处理性能,是NumPy在处理特定模式的数组操作时的强大体现。理解并掌握这种技巧,将有助于您编写更高效、更“NumPyic”的代码。
以上就是使用NumPy高效处理二维数组的2x2块操作的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号