数据中shape变换会用到的函数
前言:在处理数据的时候,经常需要存储、读取、变换等等操作,其中一个很重要的方面就是对数据进行升维和降维,如何正确的、按照我们自己的处理思路完成数据的操作非常重要,在本文中我们简单了解一些经常使用的函数。
concatenate
沿着现有的轴连接一系列数组。无论是numpy中、还是pytorch、tensorflow中,concatenate的用法都是相同的,知只是一些参数名字表达不同。在这里我们以numpy为例,结合官方文档做一些通俗易懂的解释。
numpy.concatenate((a1, a2, ...), axis=0, out=None, dtype=None, casting="same_kind")
- (a1, a2, ...):可以同时concat多个,shape要一致,除了要连接的axis那一维度(当axis参数为0,形如(m,k)和形如(n,k),在现有轴上concat后是(m+n,k)不是吗?对于本来就要连接的那一维度形状是不限制的,但是为了保证concat后的形状仍然是统一的,所以shape要保持一致)
- axis:整数,可选,默认是0。如果特别设置axis=None,会把所有数据先展平再连接。
- out:可选,指定一个已经存在的数组来存储 concatenate
操作的结果。这可以避免分配新的内存,从而在某些情况下提高性能。对新手来说还有一个功能就是用于验证concat后的shape结果是否是自己所想要达到的结果。
- dtype:可选,指定数组数据的数据类型。注意1:不能和out参数结合使用,因为out已经指定了一个数据类型。注意2:如果数组中原本的数据类型各不相同,那么指定数据类型后会区分情况,如str的内容是数值,转换到int是可以的,否则会报错,也就是说dtype会对所有的数据进行指定。
- casting:可选{‘no’, ‘equiv’, ‘safe’, ‘same_kind’, ‘unsafe’},默认为same_kind,一般和dtype配合使用,制定了在对数据进行类型转换时的规则。
(大多数场景下仅指定(a1, a2, ...)和axis参数即可)
# 简单例子 a = np.array([[1, 2], [3, 4]]) b = np.array([[5, 6]]) np.concatenate((a, b), axis=0) array([[1, 2], [3, 4], [5, 6]]) np.concatenate((a, b.T), axis=1) array([[1, 2, 5], [3, 4, 6]]) np.concatenate((a, b), axis=None) array([1, 2, 3, 4, 5, 6])
# out参数的使用方法 a = np.array([1, 2, 3]) b = np.array([4, 5, 6]) # 创建一个足够大的输出数组 out_array = np.empty((6,), dtype=int) # 确保有足够的空间和适当的数据类型 # 使用 out 参数进行合并 np.concatenate((a, b), out=out_array)
最后,当使用 np.concatenate
来合并两个或多个 MaskedArray
时,输出的数组将不再是 MaskedArray
,并且合并后的数组不会继承任何输入数组的掩码。换句话说,即使输入数组中的某些数据被掩蔽,输出数组也会将这些数据视为普通数据,不再标记为掩蔽。如果你的数据中包含 MaskedArray
并且你希望保留掩码信息,你需要在使用 np.concatenate
后手动处理或恢复这些掩码。这通常意味着你需要重新创建一个 MaskedArray
并应用原始的掩码或根据需要生成一个新的掩码。
# 创建两个 MaskedArray,其中包含一些掩蔽元素 a = ma.array([1, 2, 3], mask=[0, 1, 0]) b = ma.array([4, 5, 6], mask=[1, 0, 0]) # 使用 np.concatenate 合并这两个数组 result = np.concatenate((a, b)) # 查看合并后的结果 print("合并后的数组:", result) print("数组的掩码:", ma.getmaskarray(result)) # 这将显示没有掩码 # 重新应用掩码 new_mask = np.concatenate((a.mask, b.mask)) result_masked = ma.array(result, mask=new_mask) print("重新掩蔽后的数组:", result_masked)
最后对于使用concat的一点快速思考的trick,axis设置的那个维度是长度可以不一致的,axis所在的那个维度是各数据长度的和,其它的长度不变,如axis=1,(2,3,3)和(2,4,3)->(2,3+4,3)==(2,7,3)
stack
stack的的含义是在一个新的轴(维度)上连接一系列张量,无论是numpy中、还是pytorch、tensorflow中,stack的用法都是相同的,只是一些参数名字表达不同。
numpy.stack(arrays, axis=0, out=None, *, dtype=None, casting='same_kind')
- arrays:多个shape相同的数组,值得注意的是,这里的每个数组shape一定要一致
- axis:可选,默认为0,新添加轴所在的位置
- out: 可选,和上一个concatenate函数参数意义一致,不再赘述
- dtype:可选,指定堆叠后的数据类型
- castring: {‘no’, ‘equiv’, ‘safe’, ‘same_kind’, ‘unsafe’}, optional
与concatenate最大的不同就是,stack会产生一个新轴,新轴的长度就是堆叠前数组的个数。在pytorch中该函数的实现是基于调用concat函数,是在指定维度上unsqueeze然后再在该维度上进行concat。
所以我更觉得,在这里要理解stack后数据的组织形式,即是如何对数据进行切分和粘接的
# 从1维到2维
a = np.array([1, 2, 3]) b = np.array([4, 5, 6]) np.stack((a, b)) array([[1, 2, 3], [4, 5, 6]]) np.stack((a, b), axis=-1) array([[1, 4], [2, 5], [3, 6]])
# 从二维到三维 arrays = [np.random.randn(3, 4) for _ in range(10)] np.stack(arrays, axis=0).shape (10, 3, 4) np.stack(arrays, axis=1).shape (3, 10, 4) np.stack(arrays, axis=2).shape (3, 4, 10)
column_stack
numpy.column_stack(tup)
- tup: 一些1-D维或者2-D的数组,1-D和2-D混合也可
当输入为多维数组时,hstack
和 column_stack
的行为是一致的。但当输入数组为有且只有一维数组时,hstack
会将它们简单地并排放置,就像concat一维数组一样,而不会将其视为列向量。而column_stack会把所有的1-D数组先转换为列向量(或者简单的说是转置也一样),然后再按照列进行拼接
a = np.array((1,2,3)) b = np.array((2,3,4)) np.column_stack((a,b)) array([[1, 2], [2, 3], [3, 4]])
z = np.ones((3,3)) x = np.zeros(3) print(np.column_stack((z,x))) [[1. 1. 1. 0.] [1. 1. 1. 0.] [1. 1. 1. 0.]]
上述第一个例子如果把column_stack 替换为hstack,结果会是[1,2,3,2,3,4]
上述第二个例子如果把column_stack 替换为hstack就会报错,因为两个数组的shape不一致(不符合规则)。
row_stack
又名vstack,等同于concatenate指定了沿着第一列进行拼接
numpy.row_stack(tup, *, dtype=None, casting='same_kind')
# 1维长度相同,并返回2-D数组
a = np.array([1, 2, 3]) b = np.array([4, 5, 6]) np.vstack((a,b)) array([[1, 2, 3], [4, 5, 6]])
# 高维数组等同于concatenate指定axis为0,两个(3,1)stack后变为(6,1)
a = np.array([[1], [2], [3]]) b = np.array([[4], [5], [6]]) np.vstack((a,b)) array([[1], [2], [3], [4], [5], [6]])
expand_dims/unsqueeze
在现有的数据维度上增加一个新的维度,正确理解维度的添加方式是理解stack的的第一步、
- array_like: 输入n维数组
- axis: 整数型or元组,新增轴所在位置
# 从1维到2维
x = np.array([1, 2]) x.shape (2,)
# The following is equivalent to x[np.newaxis, :] or x[np.newaxis]: y = np.expand_dims(x, axis=0) y array([[1, 2]]) y.shape (1, 2)
# The following is equivalent to x[:, np.newaxis]: y = np.expand_dims(x, axis=1) y array([[1], [2]]) y.shape (2, 1)
当 axis
参数是一个元组时,函数会按照元组中指定的每个轴的顺序依次添加新维度。这样,你可以一次性扩展数组的多个维度,而不需要多次调用 expand_dims
。
当原始数组为(3,4):
当 axis=(0, 2)
- 首先在原数组最前面(最外层)添加一个新维度,数组形状从
(3, 4)
变为(1, 3, 4)
。 - 然后在新的第三维添加一个新维度,数组形状从
(1, 3, 4)
变为(1, 3, 1, 4)
。
当 axis=(2, 0)
- 首先在原数组的第三个轴位置(实际上是在最后,因为原数组只有两维)添加一个新维度,数组形状从
(3, 4)
变为(3, 4, 1)
。 - 然后在最外层(现在是新的第一维)添加一个新维度,数组形状从
(3, 4, 1)
变为(1, 3, 4, 1)
。
newaxis
numpy.newaxis
在 Python 的 NumPy 库中,np.newaxis
是一个非常有用的工具,用于增加数组的维度。它通常用在索引操作中,来改变现有数组的形状,从而添加一个新的轴(维度)。他和上述的expand_dims/unsequzzez的效果是一致的,我的感受是这样更显式一些。
a = np.array([1, 2, 3]) # 在数组前面添加一个新的轴,变成1x3的二维数组 row_vec = a[np.newaxis, :]
>>>
[[1,2,3]] # 在数组后面添加一个新的轴,变成3x1的二维数组 col_vec = a[:, np.newaxis]
>>>
[[1],
[2],
[3]]
有趣的是,numpy.newaxis还能作为一个None的常量/别名
print(newaxis is None) True print(np.newaxis) None
squeeze
维度压缩是expand_dims/unsqueeze的反操作,顾名思义就是在现有维度的层数上再减少一维
numpy.squeeze(a, axis=None)
- axis:None or int or tuple of ints, optional,默认参数是None
x = np.array([[[0], [1], [2]]]) x.shape (1, 3, 1) # axis=None np.squeeze(x).shape (3,) # axis=0/1/2 np.squeeze(x, axis=0).shape (3, 1) np.squeeze(x, axis=1).shape Traceback (most recent call last): ... ValueError: cannot select an axis to squeeze out which has size not equal to one np.squeeze(x, axis=2).shape (1, 3) # 所有的维度都为1 x = np.array([[1234]]) x.shape (1, 1) np.squeeze(x) array(1234) # 0d array np.squeeze(x).shape () np.squeeze(x)[()] 1234
reshape
在不更改数组数据的情况下为数组提供新形状。这个函数有点像是上述两个函数的升级款,如果想做,也是能做到上述两个函数做的事。
numpy.reshape(a, newshape, order='C')
np.reshape(a, (2, 3)) # C-like index ordering array([[0, 1, 2], [3, 4, 5]]) np.reshape(np.ravel(a), (2, 3)) # equivalent to C ravel then C reshape array([[0, 1, 2], [3, 4, 5]]) np.reshape(a, (2, 3), order='F') # Fortran-like index ordering array([[0, 4, 3], [2, 1, 5]]) np.reshape(np.ravel(a, order='F'), (2, 3), order='F') array([[0, 4, 3], [2, 1, 5]])
# order选择的区别
a = np.array([[1,2,3], [4,5,6]]) np.reshape(a, 6) array([1, 2, 3, 4, 5, 6]) np.reshape(a, 6, order='F') array([1, 4, 2, 5, 3, 6])
# 自动推算维度长度 np.reshape(a, (3,-1)) # the unspecified value is inferred to be 2 array([[1, 2], [3, 4], [5, 6]])
flatten
该函数用法直观简单且单一,就是无论什么维度的数组都展成1维的数组
ndarray.flatten(order='C')
- order: {‘C’, ‘F’, ‘A’, ‘K’}, optional
- 'C':C风格,按行的顺序展平数组(默认值)。
- 'F':Fortran风格,按列的顺序展平数组。
- 'A':如果数组是在Fortran连续内存中,按列顺序展平,否则按行顺序。
- 'K':按照元素在内存中的出现顺序展平,不考虑特定的风格。
a = np.array([[1,2], [3,4]]) a.flatten() >>> array([1, 2, 3, 4]) a.flatten('F') >>> array([1, 3, 2, 4])
总结
- concat是在现有的轴上进行连接,元素总数肯定是两个分离数组的和,维度层数不变,只有连接的那个轴的长度发生变化,且变化后的长度是变化前一些列数组的在那一维度的和。
- stack是在新建的轴上进行的连接,所以维度层数发生了变化,元素总是也是一系列被链接数组元素的和。特别地与concat不同又相同的是,只有新建的轴的长度发生变化且其它轴的shape要完全一样。
- expand_dims和squeeze都是针对某一轴长度为1的操作,不过是一个指定在那个维度层数增加一个长度为1的轴,另一个是决定将哪个长度为1的轴给删除。
- reshape也是对单个数组进行的shape重塑,reshape、expand_dims和squeeze这三者操作前后的总元素个数是不发生任何变化的。
参考链接:
chatGPT4
numpy官方文档:https://numpy.org/doc/stable/index.html