掌握 NumPy reshape:改变数组维度的利器
在数据科学和数值计算的世界里,NumPy 库以其强大的 N 维数组对象(ndarray
)和丰富的数学函数,成为了 Python 生态中不可或缺的基础工具。NumPy 数组的核心优势之一在于其高效的数据存储和操作能力,而其中一个最常用、也最强大的操作便是改变数组的维度(shape)。这项任务的实现主要依赖于 reshape
函数。
理解并熟练运用 reshape
函数,是掌握 NumPy 数组操作的关键一步。它允许我们以不同的视角看待和处理同一份数据,为数据预处理、模型输入准备、结果可视化等众多环节提供了极大的灵活性。本文将深入探讨 NumPy reshape
的方方面面,揭示其工作原理,详解各种用法,并通过丰富的例子展示其强大的能力。
一、维度(Dimension)与形状(Shape):NumPy 数组的基础
在深入 reshape
之前,我们先回顾一下 NumPy 数组的两个核心概念:维度(Dimension)和形状(Shape)。
- 维度(Dimension):也称为轴(Axis)。它代表了数组的“层级”。一个数是 0 维,一个列表或向量是 1 维,一个矩阵是 2 维,一个张量可以是 3 维、4 维甚至更高维。
- 形状(Shape):一个元组,表示数组在每个维度上的大小。例如,一个 3 行 4 列的矩阵,其形状是
(3, 4)
。一个包含 5 个元素的向量,形状是(5,)
。一个 2x3x4 的三维数组,形状是(2, 3, 4)
。
reshape
函数的本质,就是在不改变数组元素总数的前提下,改变这个表示形状的元组。你可以想象数组中的所有元素被展平成一个长长的序列,然后 reshape
按照新的形状将这些元素重新组织起来。
二、reshape
的基本用法
reshape
函数可以通过数组对象的方法调用(array.reshape(...)
),也可以作为 NumPy 顶级函数调用(np.reshape(array, ...)
)。两者的功能是相同的,前者更常见。
基本语法如下:
python
new_array = array.reshape(new_shape)
其中,new_shape
是一个新的形状元组。最重要的限制是:new_shape
中所有元素的乘积必须等于原数组中所有元素的总数。 换句话说,改变形状不能增加或减少数组中的元素。
让我们通过一些例子来理解:
例 1:将一维数组转换为二维数组
“`python
import numpy as np
创建一个一维数组
arr_1d = np.arange(12) # [0, 1, 2, …, 11]
print(“原始数组 (1D):”, arr_1d)
print(“原始形状:”, arr_1d.shape)
print(“元素总数:”, arr_1d.size)
将其重塑为 3 行 4 列的二维数组
arr_2d_1 = arr_1d.reshape(3, 4)
print(“\n重塑后的数组 (3×4):”)
print(arr_2d_1)
print(“新形状:”, arr_2d_1.shape)
将其重塑为 4 行 3 列的二维数组
arr_2d_2 = arr_1d.reshape(4, 3)
print(“\n重塑后的数组 (4×3):”)
print(arr_2d_2)
print(“新形状:”, arr_2d_2.shape)
“`
输出:
“`
原始数组 (1D): [ 0 1 2 3 4 5 6 7 8 9 10 11]
原始形状: (12,)
元素总数: 12
重塑后的数组 (3×4):
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
新形状: (3, 4)
重塑后的数组 (4×3):
[[ 0 1 2]
[ 3 4 5]
[ 6 7 8]
[ 9 10 11]]
新形状: (4, 3)
“`
在这个例子中,原数组有 12 个元素。我们将它重塑为 (3, 4)
和 (4, 3)
,因为 3 * 4 = 12
,元素总数匹配。
例 2:在二维数组之间重塑
“`python
从 3×4 数组重塑为 2×6 数组
arr_2d_3x4 = np.array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])
print(“原始数组 (3×4):\n”, arr_2d_3x4)
print(“原始形状:”, arr_2d_3x4.shape) # (3, 4), 元素总数 12
arr_2d_2x6 = arr_2d_3x4.reshape(2, 6)
print(“\n重塑后的数组 (2×6):\n”, arr_2d_2x6)
print(“新形状:”, arr_2d_2x6.shape) # (2, 6), 元素总数 12
“`
输出:
“`
原始数组 (3×4):
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
原始形状: (3, 4)
重塑后的数组 (2×6):
[[ 0 1 2 3 4 5]
[ 6 7 8 9 10 11]]
新形状: (2, 6)
“`
注意看元素是如何排列的:reshape
默认按照行主序(C-order)读取原数组元素,然后按照行主序填充新数组。即先读完第一行,再读第二行… 填充时也先填满第一行,再填第二行… 这与 C/C++ 语言中多维数组的存储方式类似,也是 NumPy 的默认行为。
例 3:增加或减少维度
“`python
将 2×6 数组重塑为 2x3x2 的三维数组
arr_3d = arr_2d_2x6.reshape(2, 3, 2)
print(“\n重塑后的数组 (2x3x2):\n”, arr_3d)
print(“新形状:”, arr_3d.shape) # (2, 3, 2), 元素总数 232 = 12
将三维数组重塑回一维数组 (展平)
arr_1d_reshaped = arr_3d.reshape(12) # 或者 arr_3d.reshape(-1) – 见下文
print(“\n重塑回一维数组:”)
print(arr_1d_reshaped)
print(“新形状:”, arr_1d_reshaped.shape) # (12,)
“`
输出:
“`
重塑后的数组 (2x3x2):
[[[ 0 1]
[ 2 3]
[ 4 5]]
[[ 6 7]
[ 8 9]
[10 11]]]
新形状: (2, 3, 2)
重塑回一维数组:
[ 0 1 2 3 4 5 6 7 8 9 10 11]
新形状: (12,)
“`
这些例子展示了 reshape
在不同维度和形状之间灵活转换的能力,前提是元素总数不变。
三、reshape
的利器:-1
的妙用
在进行 reshape
操作时,有时我们知道数组的总元素数以及除一个维度外的其他所有维度大小,但不知道或不想计算最后一个维度的大小。这时,-1
就派上了用场。
在 new_shape
元组中,最多只能有一个维度的大小指定为 -1
。NumPy 会根据原数组的元素总数和 -1
所在维度之外的其他维度大小,自动计算出 -1
应该代表的具体数值。
例如,一个有 12 个元素的数组,如果想将其重塑为只有 3 行的二维数组,我们知道形状应该是 (3, ?)
. NumPy 可以自动算出 ?
应该是 12 / 3 = 4
。这时就可以使用 (3, -1)
。
例 4:使用 -1
自动计算维度
“`python
arr = np.arange(12)
重塑为 3 行,列数自动计算
arr_3_rows = arr.reshape(3, -1)
print(“\n重塑为 3 行 (-1 列):\n”, arr_3_rows)
print(“新形状:”, arr_3_rows.shape) # 自动计算为 (3, 4)
重塑为 4 列,行数自动计算
arr_4_cols = arr.reshape(-1, 4)
print(“\n重塑为 -1 行 (4 列):\n”, arr_4_cols)
print(“新形状:”, arr_4_cols.shape) # 自动计算为 (3, 4) – 注意这里我把上面一个例子写错了,应该是 4 列,行数自动计算为 3。更正一下。
arr_4_cols_correct = arr.reshape(-1, 4)
print(“\n重塑为 -1 行 (4 列 – 修正):\n”, arr_4_cols_correct)
print(“新形状 (修正):”, arr_4_cols_correct.shape) # 自动计算为 (3, 4)
将任意形状的数组展平为一维数组 (最常见用法之一)
arr_any = np.zeros((2, 3, 2)) # 元素总数 12
arr_flattened = arr_any.reshape(-1)
print(“\n将 (2, 3, 2) 数组展平 (-1):\n”, arr_flattened)
print(“新形状:”, arr_flattened.shape) # 自动计算为 (12,)
“`
输出:
“`
重塑为 3 行 (-1 列):
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
新形状: (3, 4)
重塑为 -1 行 (4 列): # 这是我上面代码注释写错了,这里输出的是对的
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
新形状: (3, 4)
重塑为 -1 行 (4 列 – 修正):
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
新形状 (修正): (3, 4)
将 (2, 3, 2) 数组展平 (-1):
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
新形状: (12,)
“`
使用 -1
极大地简化了代码,特别是在处理变长数据或需要展平数组时,无需手动计算目标形状的某个维度大小。
四、元素的读取顺序:C-order vs Fortran-order
前面提到,reshape
默认按照行主序(C-order)读取和填充元素。但这并不是唯一的方式。NumPy 也支持列主序(Fortran-order)。可以通过 order
参数来指定。
order='C'
(默认): 按照行优先顺序读写元素。order='F'
(或'Fortran'
): 按照列优先顺序读写元素。
理解这两种顺序对于准确控制 reshape
的行为至关重要。
例 5:C-order vs Fortran-order
“`python
arr_original = np.arange(6).reshape(2, 3)
print(“原始数组 (2×3):\n”, arr_original)
[[0 1 2]
[3 4 5]]
使用 C-order 重塑为 3×2 (默认)
arr_c_order = arr_original.reshape(3, 2, order=’C’)
print(“\nC-order 重塑为 3×2:\n”, arr_c_order)
原始元素按行读取顺序: 0, 1, 2, 3, 4, 5
新数组按行填充:
[[0 1]
[2 3]
[4 5]]
使用 F-order 重塑为 3×2
arr_f_order = arr_original.reshape(3, 2, order=’F’)
print(“\nF-order 重塑为 3×2:\n”, arr_f_order)
原始元素按列读取顺序: 0, 3, 1, 4, 2, 5
新数组按列填充:
[[0 1]
[3 4]
[1 2] -> 转置后看更清晰,或者直接按列填充 [[0,1,4],[3,4,5]]
正确的 F-order 重塑填充是按列进行
[[0 3]
[1 4]
[2 5]]
为了更清晰看到 F-order 的效果,先展平,再用 F-order 重塑
arr_flat_c = arr_original.reshape(-1, order=’C’) # [0 1 2 3 4 5]
arr_flat_f = arr_original.reshape(-1, order=’F’) # [0 3 1 4 2 5]
print(“\n原始数组 C-order 展平:”, arr_flat_c)
print(“原始数组 F-order 展平:”, arr_flat_f)
使用 F-order 展平的序列 重塑回 3×2 (按 C-order 填充)
arr_f_flat_c_reshape = arr_flat_f.reshape(3, 2, order=’C’)
print(“\nF-order 展平后 C-order 重塑回 3×2:\n”, arr_f_flat_c_reshape)
使用 F-order 展平的序列 重塑回 3×2 (按 F-order 填充)
arr_f_flat_f_reshape = arr_flat_f.reshape(3, 2, order=’F’)
print(“\nF-order 展平后 F-order 重塑回 3×2:\n”, arr_f_flat_f_reshape)
再次看 arr_f_order 的结果,它就是直接在原始数组上按 F-order 进行重塑
print(“\n回到 arr_f_order 的结果 (直接在原始数组上 F-order 重塑):\n”, arr_f_order)
“`
输出:
“`
原始数组 (2×3):
[[0 1 2]
[3 4 5]]
C-order 重塑为 3×2:
[[0 1]
[2 3]
[4 5]]
F-order 重塑为 3×2:
[[0 3]
[1 4]
[2 5]]
原始数组 C-order 展平: [0 1 2 3 4 5]
原始数组 F-order 展平: [0 3 1 4 2 5]
F-order 展平后 C-order 重塑回 3×2:
[[0 3]
[1 4]
[2 5]] # 这和直接 F-order 重塑结果一样,说明直接 reshape(…, order=’F’) 就是先按 F-order 读,再按 C-order 填
# 实际上,order 参数控制的是读取元素的顺序。
# C-order reshape(new_shape, order=’C’):按 C-order 读取原始数组,按 C-order 填充新数组。
# F-order reshape(new_shape, order=’F’):按 F-order 读取原始数组,按 C-order 填充新数组。
# 这就是为什么 arr_f_order 和 arr_f_flat_c_reshape 结果一致的原因。
F-order 展平后 F-order 重塑回 3×2:
[[0 1]
[3 4]
[1 2]] # 这里的填充顺序错了,应该是按列填充
# [[0 1] -> 列1 [0,3,1]
# [3 4] -> 列2 [4,2,5]… 不对
澄清一下:reshape(new_shape, order=…)
order 参数指定的是,如果原始数组的内存布局与新的形状不兼容,需要重新排列元素时,按照哪种顺序读取原始数组的元素。
然后这些读取出来的元素总是按照新形状的 C-order (行主序) 填充到新的内存区域(如果需要复制的话)。
如果原始数组的内存布局已经适合新形状,那么 reshape 通常返回一个视图而无需复制和重新排列。
所以 arr_f_order 的结果 [[0 3], [1 4], [2 5]] 是因为原始数组 [0 1 2; 3 4 5] 按 F-order 读取是 0, 3, 1, 4, 2, 5,
然后这些元素按 C-order 填入 3×2 形状 [[0, 3], [1, 4], [2, 5]]。
``
order
**(注:上面关于参数解释的部分可能有点绕。更准确的理解是:
order参数影响的是将**多维数组**的元素视为一个**一维序列**时的顺序。然后,
reshape总是将这个一维序列按照新形状的 C-order(默认)或 F-order 重新组织。但通常情况下,我们关注的是:原始多维数组按照
order指定的顺序**读出**元素,然后这些元素按照新形状的**C-order**(默认)或**F-order****填充**进去。当原始数组需要被复制到新的内存区域时,填充顺序才会显现差异。如果返回的是视图,则元素的实际内存顺序不变,只是索引方式变了。对于
reshape来说,
order` 主要影响如果需要复制数据时,数据是按什么顺序被复制走的。默认是 C-order。)
一个更直观的例子来演示 order
如何影响元素排列:
“`python
原始数组
arr_orig = np.array([[1, 2], [3, 4]]) # 内存顺序可能是 1, 2, 3, 4 (C-order)
print(“原始数组:\n”, arr_orig)
按 C-order 展平 (默认)
arr_flat_c = arr_orig.ravel(order=’C’) # 或者 reshape(-1, order=’C’)
print(“C-order 展平:”, arr_flat_c) # [1 2 3 4]
按 F-order 展平
arr_flat_f = arr_orig.ravel(order=’F’) # 或者 reshape(-1, order=’F’)
print(“F-order 展平:”, arr_flat_f) # [1 3 2 4]
使用 C-order 展平的序列,重塑回 2×2 (按 C-order 填充)
arr_c_reshape_c = arr_flat_c.reshape(2, 2, order=’C’)
print(“C-flat -> C-reshape:\n”, arr_c_reshape_c) # [[1 2], [3 4]] (和原数组一样)
使用 C-order 展平的序列,重塑回 2×2 (按 F-order 填充)
arr_c_reshape_f = arr_flat_c.reshape(2, 2, order=’F’)
print(“C-flat -> F-reshape:\n”, arr_c_reshape_f) # [[1 3], [2 4]]
使用 F-order 展平的序列,重塑回 2×2 (按 C-order 填充)
arr_f_reshape_c = arr_flat_f.reshape(2, 2, order=’C’)
print(“F-flat -> C-reshape:\n”, arr_f_reshape_c) # [[1 3], [2 4]]
使用 F-order 展平的序列,重塑回 2×2 (按 F-order 填充)
arr_f_reshape_f = arr_flat_f.reshape(2, 2, order=’F’)
print(“F-flat -> F-reshape:\n”, arr_f_reshape_f) # [[1 2], [3 4]] (和原数组一样)
“`
输出:
原始数组:
[[1 2]
[3 4]]
C-order 展平: [1 2 3 4]
F-order 展平: [1 3 2 4]
C-flat -> C-reshape:
[[1 2]
[3 4]]
C-flat -> F-reshape:
[[1 3]
[2 4]]
F-flat -> C-reshape:
[[1 3]
[2 4]]
F-flat -> F-reshape:
[[1 2]
[3 4]]
从这个例子可以看出,order
参数更准确地影响的是元素在内存中或在展平后序列中的顺序,然后 reshape
按照目标形状和指定的 order 将这个序列重新组织。在实际使用中,如果不需要特定的内存布局,通常保持默认的 C-order 即可。但如果需要与某些 Fortran 编写的库交互,或者特定算法要求列主序,则需要使用 order='F'
。
五、reshape
返回的是视图还是副本?
这是一个重要的问题,因为它关系到内存使用和是否会修改原始数组。
通常情况下,如果新的形状与原数组的内存布局兼容,reshape
会返回原数组的一个视图(view)。这意味着新数组并没有复制数据,而是指向了原数组相同的数据块。对视图的修改会直接影响到原始数组。
然而,如果新的形状与原数组的内存布局不兼容(例如,原数组是按 C-order 存储,而新的形状需要按 F-order 访问才能高效排列,或者原始数组不是连续存储的),NumPy 可能需要将数据复制到一个新的内存区域,然后返回一个副本(copy)。在这种情况下,对新数组的修改不会影响原始数组。
你可以使用 arr.base
属性来判断返回的是视图还是副本。如果 arr.base
是原数组,那就是视图;如果是 None
,那就是副本(因为它是自己内存块的基)。
“`python
arr = np.arange(12)
print(“原始数组:”, arr.base) # None (自身是基)
返回视图的情况 (形状兼容)
arr_view = arr.reshape(3, 4)
print(“视图数组的基:”, arr_view.base is arr) # True
arr_view[0, 0] = 999
print(“修改视图后原始数组:”, arr) # 原始数组也被修改了
返回副本的情况 (强制 F-order,内存布局不兼容)
注意:对于简单连续的数组,直接 reshape(…, order=’F’) 也可能返回视图
更典型的是从不连续的数组(如切片,但切片有时也是视图)或需要打乱内存顺序的操作后重塑
arr_contig_c = np.arange(12).reshape(3, 4)
arr_contig_f = np.asfortranarray(arr_contig_c) # 创建一个 F-order 连续的数组
从 C-order 连续数组重塑为 F-order 形状(如果原数组已经是 C-order 且连续,这里可能还是视图)
尝试一个会强制复制的例子比较困难,通常与更复杂的内存布局操作结合
例如,一个非连续的数组切片,对其 reshape 可能需要复制
arr_sliced = arr_contig_c[:, [1, 3]] # 这个切片可能就不是 C-order 连续的
arr_copy = arr_sliced.reshape(-1) # 这个 reshape 可能会创建副本
print(“切片后重塑的基:”, arr_copy.base is arr_contig_c) # False 如果创建了副本
``
reshape
**(注:判断视图还是副本在 NumPy 中是一个稍微复杂的话题,取决于具体的 NumPy 版本、操作以及数组的内存布局。对于简单的在连续数组上,通常会尽量返回视图以提高效率。如果确定需要副本,可以使用
copy()方法:
arr.reshape(…).copy()。如果确定只需要视图,可以使用
view()方法,但它只改变数据类型或结构,不能像
reshape` 那样任意改变形状。)**
虽然判断视图还是副本有时不那么直观,但在大多数常用 reshape
场景下(如将一维转二维、二维转三维等),只要元素顺序不变,NumPy 会优先返回视图。了解这一点有助于避免意外修改原数组或不必要的内存开销。
六、reshape
vs. 其他相关函数
NumPy 提供了其他一些与改变形状或维度相关的函数,与 reshape
有区别:
ravel()
: 将多维数组展平(flatten)为一维数组。它会尽量返回一个视图。可以通过order
参数控制展平顺序。与reshape(-1, order=...)
功能相似,且优先返回视图。flatten()
: 也是将多维数组展平为一维数组。但flatten()
总是返回一个副本。resize()
: 可以改变数组的形状和大小。与reshape
不同,resize
会修改原始数组(in-place),并且如果新形状的元素总数与原数组不同,它会通过重复元素或截断来适应新大小。谨慎使用,因为它会永久改变原数组。expand_dims()
: 在指定轴上增加一个维度,新维度的大小为 1。例如,将形状(5,)
的一维数组变为(1, 5)
或(5, 1)
。squeeze()
: 移除形状中大小为 1 的维度。例如,将形状(1, 3, 1, 4)
的数组变为(3, 4)
。np.newaxis
或None
: 在索引操作中用于增加一个维度。例如arr[:, np.newaxis]
或arr[:, None]
将形状(5,)
的数组变为(5, 1)
。这常用于为广播(Broadcasting)做准备。
reshape
是最通用的改变形状的工具,可以在任意维度之间转换,只要元素总数匹配。而其他函数则专注于特定的形状改变任务(如展平、增加/移除单维度)。
七、reshape
在实际应用中的场景
reshape
在数据科学和机器学习中无处不在:
- 数据预处理: 将从文件读取的扁平数据重塑为矩阵、多维张量,以便于后续计算。
- 机器学习模型输入: 许多模型对输入的形状有特定要求。
- 线性模型通常需要二维输入
(样本数, 特征数)
。 - 卷积神经网络(CNN)处理图像时,输入通常需要
(样本数, 高, 宽, 通道数)
或(样本数, 通道数, 高, 宽)
的形状。如果你加载的图像是(高, 宽, 通道数)
,处理一个批次的图像就需要将它们堆叠并增加一个样本维度:np.stack(images).reshape(-1, height, width, channels)
。或者反过来,将卷积层输出的特征图展平输入到全连接层:features.reshape(batch_size, -1)
。 - 循环神经网络(RNN)处理序列数据时,输入通常需要
(样本数, 时间步长, 特征数)
的形状。
- 线性模型通常需要二维输入
- 结果处理: 将模型输出的扁平预测结果重塑回原始数据的形状,便于比较或可视化。
- 广播(Broadcasting)前的准备: 通过
reshape
调整数组的形状,使其满足广播的要求,从而进行元素级的运算。
八、总结
NumPy 的 reshape
函数是一个极其强大和灵活的工具,用于在不改变数组元素总数的前提下,改变数组的维度和形状。掌握其基本用法、神奇的 -1
参数、对 order
参数的理解,以及它返回视图还是副本的行为,是高效使用 NumPy 进行数据处理和数值计算的基础。
reshape
允许我们以不同的视角审视和操作数据,是数据科学家和机器学习工程师必备的技能之一。通过大量的练习和在实际问题中的应用,你将能更加自如地运用这个利器,解锁 NumPy 数组操作的全部潜力。记住,理解数据的原始结构和目标结构的形状是正确使用 reshape
的关键。