尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

NumPy数组合并完全指南:concatenate、vstack、hstack一次讲透

NumPy数组合并完全指南:concatenate、vstack、hstack一次讲透 你要是用 Numpy 写过几行像样的数据处理代码早晚会碰上一个绕不开的操作——矩阵合并。甭管是拼特征、叠样本、堆通道还是做批量数据增强手里握着两个形状匹配的数组第一反应就是怎么把它们合到一起。Numpy 里跟合并相关的函数一抓一把concatenate、vstack、hstack、stack、tile光看名字就能劝退一半新手。问题是这些函数长得像用起来却各有各的脾气选错了轻则报错重则数据形状错得离谱还不自知。这篇东西就是来把这些合并手段一次讲透的。我会从轴axis这个最底层也最容易绕晕的概念讲起把各个函数的适用场景、参数含义、踩坑点全部过一遍最后再给一套可以直接抄作业的实操模板。不管你是刚摸 Numpy 的初学者还是写了几年 Python 但是每次合并都得现查文档的老手这篇都能让你少翻几次报错堆栈。1. 合并之前先搞懂轴axis不然写出来的代码全靠猜1.1 轴到底是个什么东西很多人学 Numpy 的时候最劝退的就是这个 axis。它不像 Python 列表那样直观也不像 Pandas 的 DataFrame 有行列索引可以参考。我自己的经验是把轴理解成“沿着哪个方向走”就通了一半。三维数组可以想象成一个多层抽屉的衣柜轴0对应“从外往里数第几层”轴1对应“某一层里的第几行”轴2对应“某一行里的第几个格子”。合并操作的本质就是沿着某一个方向把抽屉、行或者格子拼到一起。从代码层面看更清晰。一个形状为(a, b, c)的三维数组它的三个轴分别代表三个维度上的索引范围。你在哪个轴上做合并就意味着那个维度的长度会发生变化而其他维度的长度必须严格匹配。这个规则适用于 Numpy 里几乎所有的合并函数搞懂它后面所有函数都是一个套路。比如我一个形状是(3, 4)的数组另一个形状也是(3, 4)沿着轴0合并得到的是(6, 4)——相当于在“行”这个维度上拼接列数必须一致。沿着轴1合并得到的是(3, 8)——相当于在“列”这个维度上拼接行数必须一致。1.2 为什么“维度匹配”是合并的命门很多合并报错九成都是维度匹配问题。Numpy 的报错信息还算友好会明确告诉你哪个轴的尺寸不对。但新手经常看着报错内容一头雾水就是因为不知道是哪个轴出了问题。ValueError: all the input array dimensions except for the concatenation axis must match exactly这句话翻译成大白话就是你要合并的那个轴之外的所有轴尺寸必须一模一样。打个比方你要把两排积木并成一排前提是两排积木的截面尺寸相同不然拼起来就会有凸出来的部分。Numpy 不允许这种“凸出来”的情况存在它需要的是一个规整的矩形或者长方体。还有一个容易混淆的点合并不等同于数字相加。相加要求两个数组形状完全相同而合并只要求除目标轴外的维度匹配。这也是为什么有时候数组完全不一样也能合并有时候看着差不多的反而报错的原因。提示遇到维度报错的时候先print(arr.shape)看看两个数组的形状再确认你要合并的是第几个轴90% 的问题都能在一分钟内解决。我还有一个小习惯在写合并代码之前会先在注释里写出期望的输出形状。比如# 目标形状 (6, 4)然后反推用什么函数、沿着哪个轴合并。这样写出来的代码基本不会错排查起来也快。2. 核心函数逐个拆解concatenate、vstack、hstack 和 stack 该用哪个2.1 concatenate最通用的合并工具np.concatenate是 Numpy 里最基础的合并函数其他几个函数大多在它之上做了一层封装。它的签名是numpy.concatenate((a1, a2, ...), axis0)第一个参数是一个数组的序列第二个参数指定沿哪个轴合并。直接用代码演示一下import numpy as np a np.array([[1, 2], [3, 4]]) b np.array([[5, 6], [7, 8]]) # 沿轴0合并相当于在行方向拼接 c np.concatenate((a, b), axis0) # 输出形状 (4, 2) print(c) # [[1 2] # [3 4] # [5 6] # [7 8]] # 沿轴1合并相当于在列方向拼接 d np.concatenate((a, b), axis1) # 输出形状 (2, 4) print(d) # [[1 2 5 6] # [3 4 7 8]]注意concatenate支持同时合并多个数组只要传入一个元组或者列表就行。比如np.concatenate((a, b, c), axis0)这在批量拼接多个数据块的时候非常有用不用像某些语言那样嵌套调用。不同维度的数组在concatenate里也是可以混用的——前提是沿着合并的方向你要把低维数组提升到相同维度。比如一个一维数组(4,)和一个二维数组(3, 4)你想把一维数组当作一行拼到二维数组下面得先reshape(1, 4)或者用np.expand_dims补一个维度否则 Numpy 不会自动帮你“升维”。这是我第一次用concatenate时踩过的坑试图把一个一维数组直接和一个二维数组沿着轴0合并结果报错说维度不一致。后来明白了Numpy 不会像某些编程语言那样做隐式类型转换维度不同就是不同你必须手动reshape。2.2 vstack 和 hstack两个最常用的快捷函数vstack是 vertical stack 的意思等价于沿轴0的concatenate对二维数组而言官方文档里叫“垂直堆叠”。hstack是 horizontal stack等价于沿轴1的concatenate对二维数组而言官方文档里叫“水平堆叠”。为什么有了concatenate还要出这两个快捷函数因为在实际业务场景里行拼接和列拼接占了绝大多数的需求每次写axis0、axis1容易混淆不说可读性也差。vstack和hstack从名字上就说明了拼接方向写出来的代码一目了然。# vstack 等价于 np.concatenate((a, b), axis0) v np.vstack((a, b)) # hstack 等价于 np.concatenate((a, b), axis1) h np.hstack((a, b))vstack还有一个隐藏能力它能把一维数组自动提升为二维再堆叠。比如np.vstack(([1, 2, 3], [4, 5, 6]))不需要你手动 reshape它会自动把每个一维数组当作一行处理。这个行为在拼接多组一维特征向量成为一个特征矩阵的时候特别方便。但要注意hstack对一维数组的处理就不一样了。np.hstack(([1, 2, 3], [4, 5, 6]))得到的结果是[1, 2, 3, 4, 5, 6]它是在元素级别上拼接而不是作为两行来拼接。这一点非常容易让人困惑很多新手第一次用hstack拼一维数组以为会得到一个二维矩阵结果得到一个更长的一维数组。心得如果想把两个一维数组按行堆叠成二维矩阵每个数组成为一行用np.vstack或者np.row_stack如果想把两个一维数组按列堆叠成二维矩阵每个数组成为一列先用np.column_stack或者先把每个数组reshape(-1, 1)再用hstack。vstack和hstack在代码里出现频率极高基本成了我处理二维数据时的默认选项。尤其是在做特征工程的时候要把多个特征向量拼成一个样本矩阵我几乎只用hstack特征按列拼和vstack样本按行拼。2.3 stack维度不够就给你加一层np.stack是最容易和concatenate混淆的一个函数。两者最大的区别是concatenate沿着已有的轴合并不增加维度stack会在一个全新的轴上把数组堆叠起来也就是说它会增加一个维度。举个例子两个形状都是(3, 3)的数组用concatenate沿着轴0合并结果是(6, 3)用stack沿着轴0合并结果是(2, 3, 3)——多出来的开头那个维度就是新引入的轴代表“这是第几个数组”。stack最常见的应用场景是把多个单独的矩阵堆成一个批量张量也就是给每个样本增加一个 batch 维度。在模拟项目中比如你要把10个特征矩阵(128, 64)合并成一个批次数据(10, 128, 64)stack就是为此设计的。# 两个形状都是 (3, 3) 的数组 x np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) y np.array([[9, 8, 7], [6, 5, 4], [3, 2, 1]]) # stack 沿新轴0合并结果形状 (2, 3, 3) s np.stack((x, y), axis0) print(s.shape) # (2, 3, 3) # 如果想让每个数组变成最后一维可以指定 axis-1 s2 np.stack((x, y), axis-1) print(s2.shape) # (3, 3, 2)stack的axis参数控制的是插入新轴的位置。axis0意味着新轴放在最前面axis-1意味着新轴放在最后面axis1放在第二个位置……你可以按需指定。一个经典的例子是把两个形状为(3, 3)的图像矩阵堆成(3, 3, 2)表示两张图片在同一位置上有两个通道。在图像处理相关的 Demo 里这种操作特别常见很多人会用它来构造多通道输入数据。2.4 边缘选手tile 和 repeat 竟然也算“合并”严格来说np.tile和np.repeat不是用来合并多个数组的它们是对单个数组进行复制和平铺。但在很多实际需求里它们可以替代合并操作而且有时候性能还更好。np.tile(A, reps)会把数组 A 在指定维度上像铺瓷砖一样重复铺开。np.repeat则是沿着指定轴对每个元素进行重复粒度更细。base np.array([[1, 2], [3, 4]]) # 把整个 (2,2) 数组在行方向重复2次列方向重复3次 tiled np.tile(base, (2, 3)) print(tiled.shape) # (4, 6) # 对每个元素沿着轴1重复2次 repeated np.repeat(base, 2, axis1) print(repeated.shape) # (2, 4)什么情况下该用tile而不是手动写一堆合并最常见的场景是广播模拟。比如你要给一批样本扩展成正负样本对或者要把一个模板特征复制若干份去和另一批数据对齐。直接用tile一行代码搞定还不用构造数组列表。还有一个技巧如果你想把一个一维数组当作行向量复制 N 次变成一个矩阵np.tile(vec, (N, 1))比np.vstack([vec] * N)要快代码也更短。3. 实操过程从需求到代码的一次完整演练3.1 需求拆解模拟一个数据扩充任务我们拿一个相对完整的场景来演示矩阵合并的实战用法。假设你在做一个跨平台系统的数据预处理环节手头有两个形状不同的数据来源一个是用户行为特征矩阵(200, 12)另一个是用户基础信息矩阵(200, 5)。这两个矩阵的行数相同都代表 200 个用户列代表不同的特征。需求是把两个特征矩阵合并成一个(200, 17)的大矩阵后面再接标准化和模型训练。这个需求用hstack就非常自然因为我们要做的是横向拼接把特征拼到同一行上import numpy as np # 模拟两组特征 behavior_features np.random.rand(200, 12) base_info_features np.random.rand(200, 5) # 合并成 (200, 17) all_features np.hstack((behavior_features, base_info_features)) print(all_features.shape) # (200, 17)列数变多、行数不变这就是横向拼接的正确姿势。很多教程喜欢用“把两张表格左右拼在一起”来类比hstack我觉得这个类比非常准确。3.2 批量处理场景用 stack 构造批次数据假设上面的数据预处理还不够你还要构造训练用的批次数据。原始数据是 50 个样本每个样本是一个(12, 12)的特征矩阵你需要把它们堆成一个批次张量(50, 12, 12)。批量读取到的数据如果放在一个列表里最常见的是用stack处理。模拟一下# 模拟50个样本每个是 (12, 12) 的矩阵 samples [np.random.rand(12, 12) for _ in range(50)] # 沿新轴0堆叠得到 (50, 12, 12) batch np.stack(samples, axis0) print(batch.shape)这边绝不能用vstack否则得到的是(600, 12)维度关系直接搞乱。我见过有人用vstack处理批量数据结果后面 shape 全对不上又花半天时间排查。stack在这种场景就是标准答案。因为每一个样本本身是一个矩阵它们之间没有“行”和“列”的直接关系只有“第几个样本”这一个维度上的关系。这个关系用stack来表示最准确。3.3 动态拼接先声明空数组再往里面填还有一种常见需求循环里不断产生新的结果要把它们累积起来。很多人会想到先np.array([])初始化一个空数组然后每轮循环np.append一下。这个思路在数据量小的时候没问题数据量大了性能就很差而且np.append本质上是重新创建一个数组频繁调用会带来大量内存拷贝。更好的做法是先根据预估的数量预分配一个足够大的数组或者把每次产生的结果先存到一个列表里循环结束后一次性vstack或concatenate。在实际项目中列表 最后合并的方式最自然代码也最好读。results [] for i in range(10): # 模拟每次产生一个 (4, 4) 的结果 result np.random.rand(4, 4) results.append(result) # 循环外一次性合并 final np.vstack(results) print(final.shape) # (40, 4)这个模式的好处是循环里只做简单的 append 操作不存在数组复制和扩容开销性能比每次np.append好得多。尤其是在处理几千上万次循环的场景下这个差别的体感非常明显。3.4 参数细节din和dtype的坑要提前说合并操作对 dtype 的处理是 Numpy 的一个隐形规则最后结果的 dtype 是各个输入数组 dtype 的提升结果也就是“谁大听谁的”。int32和float64合并结果是float64。int8和int16合并结果是int16。这个规则大多数时候符合直觉但有时会造成精度损失。看看这个例子a np.array([1, 2, 3], dtypenp.int32) b np.array([0.5, 1.5, 2.5], dtypenp.float64) c np.concatenate((a, b)) print(c.dtype) # float64 print(c) # [1. 2. 3. 0.5 1.5 2.5]看起来没问题但如果反过来把float64合并到int32里结果就会被截断成整数d np.concatenate((b, a)) print(d.dtype) # float64因为 b 在前面已经是 float64在合并之前检查一下每个数组的 dtype是一个很好的习惯。如果发现精度不对就用astype统一转换之后再合并。特别是从数据库或者 CSV 里读出来的数据经常混着各种 dtype最容易在这里吃亏。注意我在实际项目里遇到一个小坑两个数组都是float64合并后变成float32排查半天才发现其中一个数组之前被某段代码用astype(np.float32)转过了。所以合并之前打印一下 dtype永远值得。3.5 结合 Reshape 做复杂合并另一个高频需求是把多个一维数组拼成一个二维矩阵。这其实涉及两个操作先把每个一维数组变成行向量加一个维度再沿行方向合并。用vstack一条龙搞定前面已经提过。但如果你需要把一维数组变成列向量再横向拼接就得多个步骤了。比较优雅的写法是用np.column_stack这个函数专门把一维数组按列拼接成二维矩阵不需要手动 reshape。id_col np.array([101, 102, 103]) score_col np.array([88, 92, 75]) name_col np.array([1, 2, 3]) # 按列拼成一个 (3, 3) 矩阵 mat np.column_stack((id_col, score_col, name_col)) # array([[101, 88, 1], # [102, 92, 2], # [103, 75, 3]])如果用np.hstack((id_col.reshape(-1, 1), score_col.reshape(-1, 1), name_col.reshape(-1, 1)))也能得到同样结果但column_stack内部帮你做了 reshape代码短了不少也更不容易出错。4. 常见问题与排查技巧实录4.1 维度不匹配那个最经典的 ValueError这个报错我见过太多次了几乎每个用 Numpy 合并的人都会遇到。它的典型场景就是试图合并两个行数不一样、但想横向拼接的数组。比如一个(3, 2)的数组和一个(4, 2)的数组你用concatenate(..., axis1)Numpy 会告诉你形状不匹配。原因很简单横向拼接要求行数相同一个3行一个4行没法拼成规整的二维矩阵。排查技巧也很固定先看两个数组的 shape确认你合并的方向和轴是否对应。在合并之前写一行print(shape1, shape2)比啥都管用。还有种隐蔽的情况两个数组维度不一样。一个(3,)一个(3, 1)前者是一维后者是二维。直接hstack会得到什么(6,)吗不会实际上(3,)和(3, 1)在hstack里会被广播成(3, 3)很可能不是你想要的。这种隐形的广播合并是最容易埋雷的因为它不报错但结果完全出乎意料。4.2 合并后的数据是视图还是副本很多人关心合并结果会不会影响原数组。这个问题的答案要区分函数看concatenate、vstack、hstack、stack都会返回一个全新的数组是原来数组的拷贝不会跟原数组共享内存。所以合并后的结果随便改原数组不会变。但tile的结果也是新数组这个没问题。不过要注意如果你把合并结果赋值给变量时用切片等操作可能产生视图之后再赋值的操作会影响原数据。合并本身是安全的问题常出在后续操作上。a np.array([[1, 2], [3, 4]]) b np.array([[5, 6], [7, 8]]) c np.vstack((a, b)) c[0, 0] 999 print(a[0, 0]) # 1不受影响这个特性多数时候是好事但也意味着合并大数组时会占用额外的内存。如果你要合并的数组非常大注意内存占用情况。我在处理图像数据时经常一张图就有几十兆合并好几张内存直接飙升。如果遇到内存压力可以考虑把数据先保存在磁盘上的文件里分批处理。4.3 大量数组合并的性能拷问前面提到了列表 最后合并的模式这里再说一个更深层的性能问题循环里频繁调用concatenate或append会非常慢。因为每次都要创建一个新数组并把旧数据拷贝进去时间复杂度接近 O(n^2)。我做过一个简单基准测试import time # 慢的方式循环里 np.append arr np.zeros((0, 10)) start time.time() for i in range(10000): arr np.append(arr, np.random.rand(1, 10), axis0) print(append time:, time.time() - start) # 快的方式列表收集再 vstack arrs [] start time.time() for i in range(10000): arrs.append(np.random.rand(1, 10)) result np.vstack(arrs) print(listvstack time:, time.time() - start)在我的机器上两种方式相差几十倍不止。小数据量没什么感知到上万次循环就非常明显了。所以写代码的时候优先采用“先收集再合并”的思路这也是很多资深开发者推荐的方式。4.4 合并和广播的相爱相杀Numpy 的广播机制威力巨大但在合并的语境里很容易产生误解。比如np.array([1, 2, 3]) np.array([[4], [5], [6]])会自动广播成一个(3, 3)的矩阵。有人会想当然地认为concatenate也会做类似的自动扩展事实并非如此。广播和合并是两回事广播是在运算时临时扩展维度不改变数据本身合并是把数据实实在在地拼到一起维度必须匹配。这个区别如果不搞清楚合并的时候很容易写出让你怀疑人生的代码。一个典型例子a np.ones((3, 1)) b np.arange(6).reshape(2, 3) # 这里不能直接 concatenate因为除了合并轴外维度不匹配 # np.concatenate((a, b), axis0) # 会报错如果你需要把a扩成和b匹配的形状再合并用np.broadcast_to或者np.tile预先处理先统一形状再合。4.5 看清形状调试合并问题时最实用的三行代码当你处理复杂合并时最好的调试方法不是看报错而是打印中间结果print(a.shape:, a.shape) print(b.shape:, b.shape) print(merged.shape:, merged.shape)在 debugger 里其实也可以查看但print才是写死不变的真相。特别是在循环里合并多个数组形状一不小心就变了通过打印能快速找到是哪一步出了问题。我还习惯在合并之后用它来验证预期assert all_features.shape (200, 17), all_features.shape这种断言写法让你的代码更健壮一旦将来数据源变了形状不对程序会立刻报错而不是带着错误形状的数据往下跑。5. 最后说一点体会我自己刚接触 Numpy 合并的时候最大的纠结就是每个函数看着像但又不一样。后来踩过几次坑才慢慢总结出来先想清楚要增加维度还是保持维度再想清楚沿着哪个轴拼最后再挑函数。按这个顺序思考基本不会选错。concatenate是万能钥匙vstack和hstack是高频快捷方式stack是专门用来增加维度的tile和repeat则是把单个数组横向或纵向复制成多个。把这些函数的使用场景在脑子里过一遍再配合打印 shape 的习惯矩阵合并这种操作基本就是手到擒来。以后看到ValueError别慌先看看 shape 再打印一下顺序检查一遍轴的方向多半瞬间就明悟了。Numpy 是一个很讲逻辑的工具你只要顺着它的规则走它就不会给你出难题。
返回列表