本文主要包括numpy用法上的一些套路和坑。
ndarray的性质
axis
执行以下代码
1 | import numpy as np |
输出
1 | >>> a |
实际上表示在哪个轴上连接。比如当axis=0
的时候,最外面一层数组的长度就改变了。
可以看出,axis=0
对每个纵轴Y,求和a[:, Y]
,压扁X轴。同理,axis=1
对每个横轴求和a[X, :]
,压扁Y轴。
交换axis轴
在处理CNN相关数据的时候常常遇到类似的需求比如说NHWC和NCHW之间的交换。方法是通过swapaxes
函数。
展平
unravel_index 可以从展平后的下标回推到展平前的下标。
Order
Fortran Order和C order在打印的结果是一致的
1 | aforder = np.array([[1,2,3],[4,5,6]], order="F") |
上面的代码输出
1 | (array([[1, 2, 3], |
并且轴也是对应的
1 | print(np.sum(aforder, axis=0), np.sum(acorder, axis=0)) |
上面的代码输出
1 | (array([5, 7, 9]), array([5, 7, 9])) |
Fortran和C式存储的读写性能据说有差异,但实际看下来感觉差异不大
1 | # 计算列和 |
得到
1 | C 22499500.0 elapsed 0.00500011444092 |
dtype介绍
元素类型
dtype
作为诸如np.array
的参数时,用来表示数组中元素的类型。Numpy中的数组是homogeneous的,都具有用dtype对象表示的类型
1 | >>> type(np.int8) |
但与此同时np.dtype
还是一个函数。诸如np.int8
等都是这个类的实例。
1 | np.dtype(object, align, copy) |
如果align
为True,那么就进行对齐(似乎我们不能指定对齐到多少字节)。如果copy
为False,则是对内置数据类型对象的引用。
下面来看必选参数object,它非常灵活。
可以是类型
1
np.dtype(np.int32)
可以是字符串
这里i4
表示int32,<
表示使用小端存储。1
np.dtype('<i4')
此外,dtype还可以被直接用来描述数组,或者嵌套结构。
表示struct
含有一个字段f1,这个字段是一个struct,里面包含一个f11
2
3d = np.dtype([('f1', [('f1', np.int16)])])
d.fields
dict_proxy({'f1': (dtype([('f1', '<i2')]), 0)})表示多维数组
1
np.dtype("i4, (2,3)f8")
比较有趣的是去np.dtype
到np.void
上。
有的时候,astype(int)
会依旧输出numpy.int64
,此时参考SoF,选用.item()
ndarray的方法
创建/复制数组
复制
np.array(arr)
和np.copy(arr)
都可以复制numpy数组,感谢来自评论区的指正。
1 | arr = np.array([1,2,3,4,5]) |
以上代码输出
1 | [ 1 2 3 4 5] |
可以通过fromfunction
函数,通过index来创建一个数组。
创建随机01数组
这个通常用来生成随机的mask数组。考虑场景,我们需要把数组arr
中的随机transform_count
位进行变换f
,剩下的不变。我们可以借助下面的办法,首先对所有元素应用f
,得到transformed
数组。然后生成一个01的mask,并调用shuffle
打乱,接着将所有mask为1的位置设置为transformed
,其余的使用原来的arr
。
1 | from functools import reduce |
fromfunction
这个函数非常古怪,乍一看,我们是传入一个函数P
,这个函数接受一个坐标作为参数,输出这个坐标上的值。
但结果是这样的么?我们首先来执行一下
1 | def P(*X): |
结果输出了一个随机数
1 | def P(*X): |
结果输出
1 | [[[0 0 0 0] |
那么有什么办法可以做到呢?答案是用np.vectorize
包裹一下
1 | def P(*X): |
增删相关方法
连接
在本节中,使用下面的定义
1 | a = np.array([[1, 2], |
concatenate
concatenate表示按照axis进行连接,连接之后,axis参数对应的轴的长度会变化。如果axis是None,则拍平再连接。
1 | print np.concatenate((a, b), axis=0) |
tile
tile是concatenate的语法糖。
第二个参数表示沿着哪个轴进行复制,下面两个是等价的。
1 | print np.concatenate((a, a), axis=0) |
都返回
1 | [[1 2] |
下面的会连接到右边
1 | print np.concatenate((a, a), axis=1) |
stack
stack会升维
1 | print np.stack([a, a], axis=0) |
会返回
1 | array([[[1, 2], |
查看一下shape
1 | print a.shape, np.stack([a, a], axis=0).shape |
结果是
1 | (2L, 2L) (2L, 2L, 2L) |
append
append类似于vector
的push_back
操作
同理,如果不指定axis,那么永远是得到一个一维数组。
1 | print np.append([1, 2, 3], [[[4, 5, 6], [7, 8, 9]]], axis=None) |
View相关方法
View
相比np.array
和ndarray.copy
,numpy还提供了view的机制以方便在相同的数据上面建立view。dtype
表示元素的type,会进行reinterpret cast,而type
表示外面封装的type,比如np.array
或者np.matrix
啥的,但必须是ndarray
的子类型
1 | mat_x = np.array([(1, 2), (3, 4)], dtype=[('a', np.int8), ('b', np.int8)]) |
可以发现type
变成了matrix
,而当dtype不等于原来时,数组的含义也发生了改变。输出如下
1 | >>> print(mat_y) |
可以看出,我们对得到的数组进行了reshape,结果是跟随变化的
1 | mat_ravel = mat_x.view(dtype=np.int8).reshape(-1) |
输出
1 | [(100, 2) ( 3, 4)] |
但是flatten基于view创建了副本,所以不会随之改变
1 | mat_flatten = mat_x.view(dtype=np.int8).flatten() |
输出
1 | [(100, 2) ( 3, 4)] |
除了matrix
,还有np.recarray
这种东西,可以实现类似namedtuple或者pandas里面的类似效果
1 | named_mat_x = mat_x.view(type = np.recarray) |
输出
1 | [100 3] |
mask机制
ascontiguousarray
ndarray默认都是C Order的,也就是按行存储,那么同行之间的元素是连续的。容易想到,通过slice操作,可以得到一个既不是C连续,又不是Fortran连续的数组。
1 | a_cont = np.array([[1,2,3],[4,5,6],[7,8,9]]) |
打印一下可以发现a_non_cont
是不连续的。
1 | C_CONTIGUOUS : False |
通过ascontiguousarray
,能够得到一个底层存储不连续的数组的一个副本(有可能不是副本,但不能保证),这个新ndarray的底层存储是连续的。
1 | a_non_cont_as_cont = np.ascontiguousarray(a_non_cont) |
打印发现是C连续的
1 | C_CONTIGUOUS : True |
此外,我们还能发现np.ascontiguousarray
默认以C模式(而不是Fortran)模式输出数组。下面展示了相关代码实现,ndmin=1是为了保证C模式不出错(数组至少有一维)。
1 | ref: numpy\core\numeric.py |
不过新产生的数组就是一个副本了
1 | a_non_cont_as_cont[0][1] = 100 |
上面的代码输出
1 | [[ 2 100] |
Reshape相关方法
通过arr.shape
可以输出其形状。一般一维数组的shape是(n, )
,二维数组是(n, m)
这样。对于一维数组也可以输入一个Scalar,而不是一元tuple。
通过resize函数或者resize方法可以变更ndarray的大小,但是使用resize方法可能出现ValueError: cannot resize an array references or is referenced by another array in this way. Use the resize function
这个错误。
reshape
1 | a = np.arange(0, 10) |
输出
1 | [0 1 2 3 4 5 6 7 8 9] |
reshape之后的array的形状要能够和之前的match,例如总长度都不等,是会报错的。
1 | a2d = a.reshape((7, 7)) |
这个会报错
1 | ValueError: cannot reshape array of size 9 into shape (7,7) |
可以指定一个-1,让numpy自己推导剩下一维的大小
1 | a2d = a.reshape((2, -1)) |
输出
1 | [[0 1 2 3 4] |
但是不可以指定两个及以上的-1
1 | a3d = a.reshape((2, -1, -1)) |
这样的代码会报错
1 | ValueError: can only specify one unknown dimension |
Flatten
下面的函数都可以把数组拍平
1 | print(a2d.reshape(-1)) |
上面都是输出都是相同的
1 | [0 1 2 3 4 5 6 7 8 9] |
flatten、ravel和reshape都可以将一个多维数组压平,那么区别是什么呢?从下面的代码可以看出flatten返回的是副本。
下面的代码输出100,可见reshape不会生成副本
1 | a2d.reshape(-1)[1] = 100 |
下面的代码输出200,可见ravel也不会生成副本
1 | a2d.ravel()[1] = 200 |
下面的代码输出200,可见flattern会生成副本
1 | a2d.flatten()[1] = 300 |
Filter相关方法
去重
可以通过unique
去重
1 | all_points = np.array([1,1,2,2,3,3,4,4]) |
打印输出
1 | [1, 2, 3, 4] |
指定return_index
,会返回不重复的index而不是值
1 | _, idx = np.unique(all_points, return_index=True) |
结果如下
1 | [0 2 4 6] |
二维去重
需要注意的是,对于二维数组,也是逐元素进行去重的
1 | all_points = np.array([[1,1],[2,2],[2,2],[3,3],[3,3],[4,5]]) |
实际输出是
1 | [1, 2, 3, 4, 5] |
那么,如果我们是想把某一维作为整体进行去重,该如何做呢?下面展示一个骚操作,all_points
是一个点集类型的数据,我们希望把这个点集进行去重。主要思路是首先把它按第二维转成二进制形式
1 | b = np.ascontiguousarray(all_points.copy()) # 先复制一份出来,并将它变成连续的内存布局 |
这样我们得到的b实际是一个一维数组,每一维表示编码后的一个点。
1 | array([[b'\x01\x00\x00\x00\x01\x00\x00\x00'], # [1,1] |
然后对这个二进制编码进行去重,再反推回来。
1 | _, idx = np.unique(b, return_index=True) |
这样的输出是
1 | array([[1, 1], |
压缩
我们可以借助于unique提供的return_inverse
参数实现压缩。在指定该参数后,unique
还会返回一个数组,表示原数组中的每个元素对应到unique数组中的哪一项。
1 | u, idx = np.unique(all_points, return_inverse=True) |
在获得压缩之后,通过下面的代码会返回和all_points
一样的数组
1 | print u[idx] |
过滤
可以通过mask机制
1 | np.array([1,2,3])[np.array([False,True,False])] |
注意这里用int的结果是不同的
1 | np.array([1,2,3])[np.array([0,1,0])] |
Transform相关方法
vectorize
向量化是函数式编程中的一个默认的特性,向量化风格的写法天生直观,并且便于优化以及实现并行计算。
numpy提供vectorize
函数,用来将一个非numpy-aware的函数向量化,并运用到ndarray上。
1 | class numpy.vectorize(pyfunc, otypes=None, doc=None, excluded=None, cache=False, signature=None) |
向量化后的fminus
,等于对数列中的每个元素都进行了减100的操作
1 | a = np.arange(0, 10) |
输出
1 | [-100 -99 -98 -97 -96 -95 -94 -93 -92 -91] |
升维:onehot
在很多时候,我们会遇到升维的需求,例如要对一个一维数组做onehot得到一个二维数组,像下面这样写会报错
1 | np.vectorize(lambda x: [0,0,0,1,0])(arr_1d) |
对此,可以借助于下面的代码来实现,其中nb_classes
指的是onehot的depth,也就是说onehot维的长度。
1 | 1,1,2,3,3] arr_1d = [ |
更普遍的升维
那么有没有更通用的办法呢?爆栈网上就有人问能不能one-to-n这样映射,vectorize
主要是为了将非numpy-aware的函数提升到numpy上,而如果说要进行升维的操作,那么必然是numpy-aware的(因为相当于是ndarray套ndarray了),但其实也是有办法的,就是借助于signature
。
1 | fminus2order = np.vectorize(lambda x: np.array([x - 100]), signature='()->(n)') |
输出
1 | [[-100] |
求和和降维
vectorize
对于二维数组,也是逐元素应用的。如下所示,对一个二维数组应用一个“一维函数”,结果仿佛是先将这个数组展平,应用之后再 reshape 回去。
1 | a = np.arange(0, 10) |
但有的时候,需要用到对
1 | fsum1order = np.vectorize(lambda x: x.sum(), signature='(n)->()') |
输出就是对每一行求和
1 | [ 1, 5, 9, 13, 17] |
那么如果要对列求和怎么办呢?可以借助于 apply_over_axes
函数。apply_over_axes
函数可以指定对哪一个轴应用函数。
如下所示,我们要将二维数组中每一行的所有列加起来。下面的代码中指定1,也就是说我们要干掉第1个维度。结果正确。
1 | a |
对于一个更复杂的三维数组,它的 shape 是 (2,2,2),我希望保留中间那个维度,其他的维度都用 sum 聚合起来。实际上可以得到如下所示的结果。可以访问 https://pypi.org/project/ipfn/ 来查看具体的应用。
1 | 1, 2], [3, 4]], [[5, 6], [7, 8]]]) a = np.array([[[ |
我们也可以通过指定所有轴的方式让函数应用到所有的轴上面
1 | lambda x,axis: x+1, a2d, [0,1]) np.apply_over_axes( |
vectorize的性能问题
vectorize性能没有经过优化,所以第二行要比第一行性能好。
1 | arr = np.vectorize(lambda x: 1 if x > 0 else 0)(arr) |
Aggregate相关方法
count
numpy中没有类似np.count(value)
这样的方法,要知道这是连Python里面的原生list都有的。但是爆栈网上给出了下面的思路
1 | a = numpy.array([0, 3, 0, 1, 0, 1, 2, 1, 0, 0, 0, 0, 1, 3, 4]) |
可以看出,unique函数还是十分强大的。此外,如果只是统计0的数量,可以用下面的函数
1 | np.count_nonzero(y) |
再结合一下mask方法
1 | np.count_nonzero(y == value) |
此外,如果限定是大数据量,但是值域很小并且是整数的场景,还可以用bincount
来解决。
all和any
all指定axis会降维。如果不带参数,会返回scalar。
1 | a = np.array([[True, True], |
如果指定keepdims
,那么返回的数组保持和原数组一样的维度。
1 | print a.all(keepdims=True) |
我们考虑sum方法,下面的两个语句都是按行相加,但一个返回二维数组,一个返回一维数组。
1 | print(np.sum(a, axis=1, keepdims=True)) |
argpartitions
这个函数的一个应用是求出一个 n 维数组中,前 want 大的元素对应的下标。其做法是先 ravel 成一维数组,并 argpartition 选出第 -want 个元素,然后再调用 unravel_index
找到展平前对应的下标。
1 | def get_top_indices(a, want): |
广播
1 | print np.array([3]) * 2 |
但是下面的代码会报错
1 | print np.array([3,7]) * np.array([2,4,6]) |
显示
1 | ValueError: operands could not be broadcast together with shapes (2,) (3,) |
ndarray对Python函数的亲和性
原生函数
len
一般返回ndarray的行数zip
zip作用到两个一维ndarray,会得到一个原生的list。1
2
3
4
5z1 = np.array([1,2,3,4,5])
z2 = np.array([6,7,8,9,10])
z = zip(z1, z2)
print(z)
print(type(z))输出
1
2[(1, 6), (2, 7), (3, 8), (4, 9), (5, 10)]
<type 'list'>如果说zip两个不同维度的呢?
1
list(zip(np.array([[1,2],[3,4]]), np.array([5,6])))
输出
1
[(array([1, 2]), 5), (array([3, 4]), 6)]
any和all
注意,numpy中也有any
和all
,例如(A==B).all()
返回A和B中是否所有的数都对应相等。itertools
1
2import itertools
print(list(itertools.imap(lambda x, y: x * y, z1, z2)))
原生类型转换
ndarray和原生数组/常量的互转
pandas类型转换
详见pandas相关
matplotlib
一个程序中,所有的plot默认会画到一个figure上。包括使用importlib的调用。所以应当使用plt.figure(x)
来选择绘制的图,并使用close(x)
进行关闭。