MATLAB

reshape(A, m, n) 矩阵变形:3×4 变 2×6,元素按列重排的规则示例

👤 为我痴狂 👁 2 阅读 ❤ 0 点赞 ➦ 0 分享 📅 2026-10-11
首页› 理学› MATLAB› 正文
reshape(A, m, n) 矩阵变形:3×4 变 2×6,元素按列重排的规则示例

从内存线性地址出发,把“按列重排”讲透:索引映射、行主序/列主序之争、零拷贝视图、跨框架语义差异与工程落地清单

摘要

reshape 是数组编程里最“不起眼”却最容易踩坑的操作之一。一个 3×4 的矩阵要变成 2×6,元素到底怎么排?为什么 MATLAB 和 NumPy 给出的结果不一样?为什么 PyTorch 里一个 view 有时报错、有时又静默复制?本文以 reshape(A, m, n) 为切入点,沿着“线性存储 → 索引映射 → 视图/拷贝 → 工程实现”这条主线,把列优先重排的规则讲清楚,并给出可复现的验证步骤、性能优化路径与跨语言对照表。

本文评述:reshape 的本质不是“改变形状”,而是“在不改变底层线性顺序的前提下,重新解释下标到地址的映射函数”。理解这一点,绝大多数困惑都会迎刃而解。

一、问题的起点:3×4 变 2×6 到底发生了什么

假设有一个 3 行 4 列的矩阵 A,元素为 1 到 12。现在要把它 reshape 成 2 行 6 列。很多人的第一反应是“按行读、按行写”,于是得到一种结果;但如果你在 MATLAB 或 Fortran 里跑,会得到另一种结果。这不是 bug,而是“谁在定义线性顺序”的问题。

先看一个最直观的对比。设 A 为:

A =
[  1   4   7  10
   2   5   8  11
   3   6   9  12 ]

注意这里写的是“列优先视角”下的排列:第一列是 1,2,3,第二列是 4,5,6,以此类推。如果按列优先(column-major)线性化,得到序列:

1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12

把这 12 个数按“每列 2 个元素”重新填进 2×6 的矩阵,就得到:

B =
[  1   3   5   7   9  11
   2   4   6   8  10  12 ]

这就是 MATLAB 中 reshape(A, 2, 6) 的标准结果。而如果按行优先(row-major)线性化,序列是 1,4,7,10,2,5,8,11,3,6,9,12,重新按每行 6 个填入,得到的是另一种矩阵。

关键结论:reshape 的结果取决于底层线性顺序的定义。同一份数据、同一个目标形状,在不同语言/库中可能得到不同矩阵,而它们都“正确”。

笔者认为,把 reshape 理解成“重新排列元素”是一种误导。更准确的说法是:reshape 保持底层一维缓冲区不变,只改变“下标 → 偏移量”的映射函数。所谓“按列重排”,其实是列优先映射函数在新形状上的自然结果。

二、线性存储与索引映射:reshape 的数学骨架

2.1 多维数组的两种经典布局

计算机内存是一维的。一个 m×n 的二维数组要放进一维地址空间,必须约定一个展开顺序。历史上形成了两大阵营:

布局 展开顺序 代表语言/库 A(i,j) 的线性偏移
列优先 Column-major 先列后行 Fortran、MATLAB、Julia、BLAS/LAPACK i + j·m(0-based)
行优先 Row-major 先行后列 C/C++、NumPy(默认)、PyTorch(默认) i·n + j(0-based)

本文评述:布局之争本质上是“哪个下标变化最快”的约定。列优先让同一列的元素在内存中相邻,行优先让同一行的元素相邻。这个选择会一路影响到缓存命中率、向量化指令、甚至深度学习框架的算子实现。

2.2 步长(stride)视角:更通用的描述

现代数组库(NumPy、PyTorch、TensorFlow)用“步长”来描述布局。一个形状为 (d₀, d₁, …, d_{k-1}) 的数组,其步长 (s₀, s₁, …, s_{k-1}) 表示每个维度下标增加 1 时,线性偏移量增加多少。元素 (i₀, i₁, …, i_{k-1}) 的偏移为:

offset = Σ iₖ · sₖ

对于 C 连续(C-contiguous)的 m×n 数组,步长为 (n, 1);对于 Fortran 连续的 m×n 数组,步长为 (1, m)。reshape 要做的,就是给定新的形状 (m', n'),找到一组新步长 (s₀', s₁'),使得新旧映射覆盖同一段线性缓冲区。

本文评述:步长是理解 reshape 是否“零拷贝”的钥匙。只要新形状能由原步长通过某种整除关系推导出来,reshape 就可以是视图;否则就必须复制。这一点在第六节会展开。

三、列优先 vs 行优先:一次彻底说清

3.1 用“读入顺序”和“写出顺序”两个动作来拆解

很多人混淆的根源,是把“读”和“写”的顺序混在一起。reshape 可以拆成两步:

  1. 读入:按原数组的布局,把元素线性化成一个序列;
  2. 写出:按目标数组的布局,把这个序列填回多维结构。

如果读写都按列优先,结果就是 MATLAB 风格;如果读写都按行优先,结果就是 C/NumPy 风格。真正容易出错的是“读按列、写按行”这种混合场景——它对应的是转置加 reshape,而不是单纯的 reshape。

3.2 一个容易记的口诀

列优先:先竖着走完一列,再走下一列。
行优先:先横着走完一行,再走下一行。
reshape:线性序列不变,只换“每行/每列装几个”。

笔者认为,口诀的价值在于把抽象约定变成可操作的肌肉记忆。当你在调试一个跨语言的数据管道时,先问自己“源端和目标端各自按什么顺序线性化”,比死记公式更可靠。

四、手算全过程:3×4 → 2×6 的逐步推演

4.1 列优先场景(MATLAB / Fortran)

原始 3×4 矩阵,按列优先线性化:

线性位置 1 2 3 4 5 6 7 8 9 10 11 12
元素 1 2 3 4 5 6 7 8 9 10 11 12

目标 2×6 矩阵按列优先填充,即每列 2 个元素。第 1 列放 1、2;第 2 列放 3、4;……第 6 列放 11、12。得到:

B =
[  1   3   5   7   9  11
   2   4   6   8  10  12 ]

验证:B(1,1)=1,B(2,1)=2,B(1,2)=3,B(2,2)=4,符合“每列两个”的填充规则。

4.2 行优先场景(C / NumPy / PyTorch)

同样的 3×4 矩阵,按行优先线性化:1,4,7,10,2,5,8,11,3,6,9,12。目标 2×6 按行优先填充,每行 6 个:

B =
[  1   4   7  10   2   5
   8  11   3   6   9  12 ]

可以看到,两种布局给出的 B 完全不同。但它们都满足“元素总数守恒”和“线性缓冲区不变”这两个硬约束。

4.3 索引映射公式的推导

对于列优先,原数组 A 是 m×n(m=3, n=4),目标 B 是 p×q(p=2, q=6),且 m·n = p·q = 12。元素 A(i, j)(0-based)的线性偏移为 i + j·m。它在新数组中的位置 (i', j') 满足:

i + j·m = i' + j'·p

由此可解出 i' = (i + j·m) mod p,j' = (i + j·m) div p。这就是列优先 reshape 的完整映射公式。

本文评述:这个公式是全文的“心脏”。它说明 reshape 不涉及任何数据搬移的语义,只涉及下标的重解释。工程上所有优化,都是围绕“如何避免真的搬数据”展开的。

五、跨语言对照:C、NumPy、MATLAB、PyTorch、Julia

5.1 各语言默认顺序与 reshape 行为

环境 默认顺序 reshape 接口 是否可指定顺序
C / C++ 行优先 无内置,需手动索引 —
NumPy 行优先 np.reshape / ndarray.reshape order='C'/'F'/'A'
MATLAB 列优先 reshape(A, m, n) 默认列优先
PyTorch 行优先 torch.reshape / Tensor.view view 要求连续
Julia 列优先 reshape(A, m, n) 默认列优先

5.2 NumPy 的 order 参数实战

在 NumPy 中,要复现 MATLAB 的列优先结果,需要显式指定 order='F':

import numpy as np
A = np.arange(1, 13).reshape(3, 4, order='F')
B = A.reshape(2, 6, order='F')
print(B)

输出为:

[[ 1  3  5  7  9 11]
 [ 2  4  6  8 10 12]]

这与 MATLAB 完全一致。反之,如果不指定 order,默认行优先会得到另一组值。

本文评述:NumPy 的 order 参数是跨语言互操作的“翻译器”。在数据管道中,建议显式写出 order,而不是依赖默认值,否则代码在跨团队、跨语言迁移时极易产生静默错误。

5.3 MATLAB 与 NumPy 互转的常见坑

从 MATLAB 导出 .mat 文件到 Python 时,scipy.io.loadmat 默认会保留列优先语义,但读入后数组的 strides 可能是 Fortran 连续的。此时若直接调用 reshape 而不指定 order,结果会与 MATLAB 不一致。建议的处理路径:

  1. 读入后先用 np.asfortranarray 明确布局;
  2. 所有 reshape 显式带 order='F';
  3. 写回 .mat 前用 np.ascontiguousarray 或对应转换;
  4. 用一组已知小矩阵做单元测试,锁定语义。

六、视图还是拷贝:零拷贝 reshape 的判定条件

6.1 连续数组的 reshape 一定是视图吗

对于 C 连续数组,reshape 成任意形状(只要元素总数不变)通常可以返回视图,因为新步长可以由原步长整除推导。NumPy 的文档指出,当新形状与内存布局兼容时,reshape 返回视图;否则返回副本(来源:NumPy 官方文档 reshape 条目)。

但“通常”不等于“总是”。如果原数组是转置或切片得到的非连续数组,reshape 往往需要复制。例如:

A = np.arange(12).reshape(3, 4)
At = A.T          # 形状 4x3,非连续
B = At.reshape(2, 6)  # 可能触发拷贝

可以用 B.base is A 或 np.shares_memory(A, B) 来判断是否共享内存。

6.2 PyTorch 的 view 与 reshape 差异

PyTorch 中,view 要求张量内存连续,否则报错;reshape 更宽容,必要时会复制。这一设计在 PyTorch 官方文档中有明确说明。工程建议:

  • 明确知道张量连续时,用 view 以表达“零拷贝”意图;
  • 不确定时用 reshape,让框架决定;
  • 在性能敏感路径上,先用 tensor.is_contiguous() 检查,再决定调用哪个。

本文评述:view 与 reshape 的差异,本质上是“把布局约束交给调用者还是库”。前者更可控但更脆弱,后者更稳健但有隐式拷贝成本。笔者认为,在模型推理的热路径上,宁可多写一行 is_contiguous 检查,也不要让隐式拷贝悄悄吃掉带宽。

七、工程陷阱:转置、切片、广播与内存布局

7.1 转置 + reshape ≠ reshape + 转置

这是最经典的坑。设 A 为 3×4,考虑两种操作序列:

路径一:B1 = reshape(A.T, 2, 6)
路径二:B2 = reshape(A, 2, 6).T

两者通常不相等。原因是转置改变了线性顺序的读取方式,而 reshape 又在这个新顺序上重新解释。工程上,凡是涉及“转置 + reshape”的组合,都应画一张内存布局图确认。

7.2 切片导致的非连续

对数组做步长不为 1 的切片(如 A[::2])会得到非连续视图。此时 reshape 往往触发拷贝。在 NumPy 中可用 A.flags['C_CONTIGUOUS'] 检查;在 PyTorch 中用 is_contiguous()。

7.3 广播与 reshape 的交互

广播(broadcasting)会创建步长为 0 的维度。这类数组 reshape 时几乎必然复制,因为步长 0 无法映射到新形状的整除关系。实践中,建议在广播之后先 copy() 或 contiguous(),再做 reshape,避免隐式行为难以追踪。

7.4 一个可复现的调试脚本

import numpy as np

def report(name, arr):
    print(f"{name}: shape={arr.shape}, "
          f"C={arr.flags['C_CONTIGUOUS']}, "
          f"F={arr.flags['F_CONTIGUOUS']}, "
          f"strides={arr.strides}")

A = np.arange(1, 13).reshape(3, 4)
report("A", A)
report("A.T", A.T)
B = A.reshape(2, 6)
report("B", B)
print("shares memory:", np.shares_memory(A, B))

运行这段脚本,可以直观看到 strides 与连续性标志的变化,是排查 reshape 问题的第一手工具。

八、性能视角:reshape 的代价与优化路径

8.1 零拷贝 reshape 的成本几乎为零

当 reshape 返回视图时,其时间复杂度为 O(1),只修改元数据(shape、strides)。这在 NumPy、PyTorch 中都是如此。因此,在数据预处理管道中,优先设计成“视图友好的”操作序列,可以显著减少内存带宽消耗。

8.2 触发拷贝时的成本模型

一旦触发拷贝,成本为 O(N),N 为元素总数。对于 float32 的 3×4 小矩阵,拷贝成本可忽略;但在大张量场景(如 batch size 1024、通道 256、特征图 56×56),一次隐式拷贝可能意味着数百 MB 的内存流量。工程上应通过 profiler 确认是否发生拷贝。

操作 是否拷贝 复杂度 典型场景
连续数组 reshape 否(视图) O(1) 展平、加维
非连续 reshape 是 O(N) 转置后 reshape
广播后 reshape 是 O(N) expand 后展平
order='F' 与默认不一致 可能拷贝 O(N) 跨语言互操作

8.3 优化路径清单

  1. 在管道设计阶段,尽量让 reshape 紧跟在“连续化”操作之后;
  2. 用 contiguous() / ascontiguousarray 显式控制布局,避免隐式拷贝;
  3. 对热点路径做内存流量 profiling,确认 reshape 是否成为瓶颈;
  4. 在跨语言边界,统一约定 order,减少转换次数;
  5. 对固定形状的小矩阵,考虑用编译期常量展开索引计算。

本文评述:reshape 的性能问题,90% 来自“隐式拷贝”。把隐式变显式,是工程优化的第一步。笔者认为,团队内部应建立一条规范:凡是可能触发拷贝的 reshape,必须在代码注释中说明原因和预期成本。

九、前沿与预判:从 BLAS 到深度学习算子

9.1 BLAS/LAPACK 的列优先遗产

BLAS 和 LAPACK 作为数值线性代数的基石,长期采用列优先布局。这源于 Fortran 的历史选择。NumPy 在与 BLAS 交互时,会尽量把数组转成 Fortran 连续,以复用底层例程。这意味着,一个看似简单的 reshape,可能触发一次布局转换,进而影响矩阵乘法的性能。

本文评述:列优先并非“过时”,而是被高性能库深度绑定。理解这一点,有助于解释为什么在某些 NumPy 工作负载中,显式使用 order='F' 反而更快。

9.2 深度学习框架中的 reshape 算子

在 PyTorch、TensorFlow 等框架中,reshape 通常被实现为“元数据操作 + 必要时拷贝”。在计算图层面,reshape 可能被融合进相邻算子,减少中间张量。近年的编译器(如 TorchInductor、XLA)会尝试消除冗余 reshape,把连续的 reshape 合并成一次布局变换。

相关研究方面,TVM、MLIR 等编译器基础设施都在布局变换与算子融合上做了大量工作。这些系统的共同思路是:把 reshape 视为布局约束,在编译期求解最优的布局传播方案。由于该领域文献更新极快,建议读者直接查阅各项目官方文档与论文库获取最新进展。

9.3 对工程实践的预判

  • 未来框架会更激进地做 reshape 消除,开发者应减少对“reshape 后内存布局”的隐式依赖;
  • 跨语言数据交换会更多采用显式布局描述(如 Arrow、DLPack),减少 order 歧义;
  • 在边缘设备上,零拷贝 reshape 的价值会进一步放大,因为内存带宽更稀缺。

十、可复现验证清单与学习资源

10.1 五步验证法

  1. 构造 1..12 的 3×4 矩阵,打印其 strides 与连续性标志;
  2. 分别用行优先和列优先 reshape 成 2×6,记录结果;
  3. 用 shares_memory 检查是否视图;
  4. 对转置、切片、广播三种输入重复上述步骤;
  5. 把结果与 MATLAB/Julia 对照,确认语义一致。

10.2 推荐学习资源

十一、主要参考文献

[1] NumPy Developers. numpy.reshape — NumPy v2.x Manual. 2024. https://numpy.org/doc/stable/reference/generated/numpy.reshape.html

[2] NumPy Developers. Internal organization of NumPy arrays. 2024. https://numpy.org/doc/stable/dev/internals.html

[3] PyTorch Contributors. torch.reshape — PyTorch Documentation. 2024. https://pytorch.org/docs/stable/generated/torch.reshape.html

[4] MathWorks. reshape — Reshape array. MATLAB Documentation. 2024. https://www.mathworks.com/help/matlab/ref/reshape.html

[5] The Julia Language. Multi-dimensional Arrays. Julia Documentation. 2024. https://docs.julialang.org/en/v1/manual/arrays/

[6] Netlib. BLAS (Basic Linear Algebra Subprograms). 2023. https://www.netlib.org/blas/

[7] Apache TVM. TVM Documentation — Layout Transformations. 2024. https://tvm.apache.org/docs/

[8] LLVM Project. MLIR — Multi-Level Intermediate Representation. 2024. https://mlir.llvm.org/

[9] Harris C R, Millman K J, van der Walt S J, et al. Array programming with NumPy. Nature, 2020, 585: 357–362.

文章声明

本文内容仅为作者学习、思考、经验、笔记的总结,仅供技术交流与参考。文中观点仅代表笔者个人思辨,不构成任何学术建议、商业建议或专业建议。所有数据来源已标注,引用时请以原始文献为准。

内容仅供学习参考。如需引用,请以原始文献为准。  全文约 12600 字 | 参考文献 60 篇(主要 9 篇)
🔒 复制本站文章内容需登录并达到 L3。当前:未登录

分享到

💬
微信
📷
朋友圈
🐧
QQ好友
🌐
QQ空间
👁
微博
📌
钉钉
🔗
复制链接
📑
复制图文

微信扫一扫分享

打开微信「扫一扫」,扫描二维码后在微信中分享给好友或朋友圈。

💬 评论 (0)

评论功能已关闭

⏸️ 本站暂未开放评论功能,不能进行评论,此为规划的后续开发预留
首页| 关于本网| 网站声明| 联系我们| 网站纠错| 服务| 网站地图
黔ICP备19010680号-1  |  邮箱:six528528@163.com
贵公网安备 52010302001819号
Copyright 2019-2026 http://www.databrush.com/ All rights reserved.
QQ
QQ扫一扫
Logo
DBN数据刷