NumPy高效使用逻辑,11个角度理顺它!

  • 2019 年 11 月 22 日
  • 筆記

1 Numpy更高效

使用Python的地方,就能看到Numpy,尤其是需要数值计算的地方,Numpy的高性能更是体现的淋漓尽致。

它基于Python,提供远高于Python的高性能向量、矩阵和更高维度的数据结构。之所以性能高是由于它在密集型计算任务中,向量化操作是用C和Fortran代码实现。

2 导入Numpy

只需要一行代码就能导入:

from numpy import *

在numpy包中,描述向量,矩阵和更高维度的数据集使用的术语是array.

3 生成numpy数组

有许多方法能初始化一个新的numpy数组,例如:arange, linspace等,从文件中读入数据,从python的lists等都能生成新的向量和矩阵数组。例:

In [1]: from numpy import *    In [2]: v = array([1,2,3,4])    In [3]: v  Out[3]: array([1, 2, 3, 4])    In [4]: m = array([[1,2],[3,4]])    In [5]: m  Out[5]:  array([[1, 2],         [3, 4]])

v和m的类型都是ndarray,这是numpy中最主要的数据结构之一

In [6]: type(v),type(m)  Out[6]: (numpy.ndarray, numpy.ndarray)

v和m的不同仅仅是它们的形状(shape), 我们能通过ndarray.shape属性发现它们的形状信息,shape属性很有用,尤其在深度学习模型调试中:

In [7]: shape(v),shape(m)  Out[7]: ((4,), (2, 2))

numpy中获取元素个数通过size:

In [8]: size(v),size(m)  Out[8]: (4, 4)

4 为什么要用numpy?

到此,numpy.ndarray看起来非常像Python的list, 那我们为什么不用Python的list计算,干嘛非要创造一个新的数组(array)类型呢?

有多个原因:

  • Python的list是一个通用结构。Python的list能包括任意类型的对象,并且是动态类型, 它们不支持一些数学函数,比如矩阵的点乘,实现如此的函数对于Python的list而言,不会高效,因为它是动态类型
  • Numpy的array是静态类型和同质的,当array被创建时,元素的类型就确定
  • Numpy的array更节省内存
  • 由于是静态类型,一些数学函数实现起来会更快,例如array间的加减乘除能够用C和Fortran实现

使用ndarray.dtype, 我们能看到一个数组内元素的类型:

In [9]: m.dtype  Out[9]: dtype('int32')

如果我们尝试用str类型赋值给m,会报错:

In [10]: m[0,0]='hello'  ---------------------------------------------------------------------------  ValueError                                Traceback (most recent call last)  <ipython-input-10-8d5580112ac6> in <module>  ----> 1 m[0,0]='hello'    ValueError: invalid literal for int() with base 10: 'hello'

创建数组时,能指定类型,通过为dtype赋值:

In [11]: mc = array([[1,2,],[3,4]],dtype=complex)    In [12]: mc  Out[12]:  array([[1.+0.j, 2.+0.j],         [3.+0.j, 4.+0.j]])

dtype更多取值:int, float, complex, bool, object, 我们还可以显示的定义数据位数的类型,如:int64, int16, float128, complex128

5 通过函数生成数组

对于更大的数组,手动初始化数据是不现实的,比如使用python的list. 我们得用numpy提供的函数才能生成不同形式的数组。比如更通用的:

arange 函数:起始点,终点,步长;不包括终点

In [2]: x = arange(0,10,1)  In [3]: x  Out[3]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])    In [4]: x = arange(-1, 1, 0.1)  In [5]: x  Out[5]:  array([-1.00000000e+00, -9.00000000e-01, -8.00000000e-01, -7.00000000e-01,         -6.00000000e-01, -5.00000000e-01, -4.00000000e-01, -3.00000000e-01,         -2.00000000e-01, -1.00000000e-01, -2.22044605e-16,  1.00000000e-01,          2.00000000e-01,  3.00000000e-01,  4.00000000e-01,  5.00000000e-01,          6.00000000e-01,  7.00000000e-01,  8.00000000e-01,  9.00000000e-01])

linspace 函数:起始点,终点,分割份数;包括终点

In [5]: linspace(0,10,5)  Out[5]: array([ 0. ,  2.5,  5. ,  7.5, 10. ])

logspace 函数:如下例子,各项分别为 e^1,e^2, e^3,…e^10

In [17]: logspace(1, 10, 10, base=e)  Out[17]:  array([2.71828183e+00, 7.38905610e+00, 2.00855369e+01, 5.45981500e+01,         1.48413159e+02, 4.03428793e+02, 1.09663316e+03, 2.98095799e+03,         8.10308393e+03, 2.20264658e+04])

mgrid 函数,实际工作中也很有用,在这里我列举一个

In [18]: x,y = mgrid[0:5,0:5]    In [19]: x  Out[19]:  array([[0, 0, 0, 0, 0],         [1, 1, 1, 1, 1],         [2, 2, 2, 2, 2],         [3, 3, 3, 3, 3],         [4, 4, 4, 4, 4]])    In [20]: y  Out[20]:  array([[0, 1, 2, 3, 4],         [0, 1, 2, 3, 4],         [0, 1, 2, 3, 4],         [0, 1, 2, 3, 4],         [0, 1, 2, 3, 4]])

这是基本用法,完全看不出干啥。如果我有10个点,想要得出这10个点的两两间距离:

x,y = mgrid[0:5,0:5]  In [28]: list(map(lambda xe,ye: [(ex,ey) for ex, ey in zip(xe, ye)], x,y))  Out[28]:  [[(0, 0), (0, 1), (0, 2), (0, 3), (0, 4)],   [(1, 0), (1, 1), (1, 2), (1, 3), (1, 4)],   [(2, 0), (2, 1), (2, 2), (2, 3), (2, 4)],   [(3, 0), (3, 1), (3, 2), (3, 3), (3, 4)],   [(4, 0), (4, 1), (4, 2), (4, 3), (4, 4)]]

特殊点的矩阵,对角阵:

In [22]: diag([1,2,3])  Out[22]:  array([[1, 0, 0],         [0, 2, 0],         [0, 0, 3]])

主对角线偏移1的矩阵:

In [23]: diag([1,2,3],k=1)  Out[23]:  array([[0, 1, 0, 0],         [0, 0, 2, 0],         [0, 0, 0, 3],         [0, 0, 0, 0]])

零阵:

In [24]: zeros((3,3))  Out[24]:  array([[0., 0., 0.],         [0., 0., 0.],         [0., 0., 0.]])

1阵:

In [25]: ones((3,3))  Out[25]:  array([[1., 1., 1.],         [1., 1., 1.],         [1., 1., 1.]])

6 随机函数

Numpy提供的random使用起来更友好,如下:

获得取值(0,1)上的均匀分布,生成shape为(1,2,3)的三维数组:

In [36]: random.rand(1,2,3)  Out[36]:  array([[[0.22120906, 0.80388812, 0.52726328],          [0.37277153, 0.88388754, 0.76586961]]])

满足均值为0,方差为1的高斯分布得到随机数据,生成shape为(5,)的一维数组:

In [38]: random.randn(5)  Out[38]: array([ 0.51924157,  1.05994257, -0.492707  ,  0.87205736, -0.14581919])

7 主要属性

生成1个三维数组:

In [32]: M = random.rand(2,3,4)  In [33]: M  Out[33]:  array([[[0.92362312, 0.68582456, 0.95927478, 0.40126712],          [0.13534077, 0.42983011, 0.72604572, 0.64202846],          [0.96822191, 0.8332197 , 0.64065175, 0.8979606 ]],           [[0.62970371, 0.22183503, 0.58752818, 0.20008916],          [0.21533794, 0.20558915, 0.751807  , 0.07743367],          [0.09854002, 0.55408343, 0.8663579 , 0.150306  ]]])

每个元素的字节数:

In [28]: M.itemsize  Out[28]: 8

M的总字节数:

In [30]: M.nbytes  Out[30]: 192

M的维数:

In [29]: M.ndim  Out[29]: 3

8 索引数组

索引数组的元素,可以使用方括号和下标,M是三维,下标索引的方法:

In [38]: M[1,1,2]  Out[38]: 0.7518069979719579

使用 表示全部此维度的所有元素都要获取:

In [41]: M[0,:,:]  Out[41]:  array([[0.92362312, 0.68582456, 0.95927478, 0.40126712],         [0.13534077, 0.42983011, 0.72604572, 0.64202846],         [0.96822191, 0.8332197 , 0.64065175, 0.8979606 ]])

也可方便的切片:

In [47]: M[:,:2,1:3]  Out[47]:  array([[[1.        , 0.95927478],          [1.        , 0.72604572]],           [[0.22183503, 0.58752818],          [0.20558915, 0.751807  ]]])

直接赋值:

In [43]: M[0,:,1]=1.0  In [44]: M  Out[44]:  array([[[0.92362312, 1.        , 0.95927478, 0.40126712],          [0.13534077, 1.        , 0.72604572, 0.64202846],          [0.96822191, 1.        , 0.64065175, 0.8979606 ]],           [[0.62970371, 0.22183503, 0.58752818, 0.20008916],          [0.21533794, 0.20558915, 0.751807  , 0.07743367],          [0.09854002, 0.55408343, 0.8663579 , 0.150306  ]]])

更有用的掩码索引,对应一个bool类型的数组:

In [49]: M[0,:,[False,True,True,False]]  Out[49]:  array([[1.        , 1.        , 1.        ],         [0.95927478, 0.72604572, 0.64065175]])

M的维数:2*3*4,结果维度预期:1*3*2,但是实际结果维度:1*2*3

掩码索引,这一特性对于带条件的选取元素很重要。例如,使用arange生成一维数组x:

In [51]: x  Out[51]:  array([0. , 0.5, 1. , 1.5, 2. , 2.5, 3. , 3.5, 4. , 4.5, 5. , 5.5, 6. ,         6.5, 7. , 7.5, 8. , 8.5, 9. , 9.5])

判断x 中哪些元素大于5 呢

In [52]: x>5

结果返回的一堆:True, False:

Out[52]:  array([False, False, False, False, False, False, False, False, False,         False, False,  True,  True,  True,  True,  True,  True,  True,          True,  True])

然而有时候,我们想知道x 中哪些位置的元素大于5,此时要借助 where :

In [53]: where(x>5)  Out[53]: (array([11, 12, 13, 14, 15, 16, 17, 18, 19], dtype=int64),)

9 元素级操作

NumPy中两个数组加减乘除等,默认都是对应元素的操作:

In [55]: v1  Out[55]: array([0, 1, 2, 3, 4])

v1+2:

In [57]: v1+2  Out[57]: array([2, 3, 4, 5, 6])

v1 * v1:

In [58]: v1 * v1  Out[58]: array([ 0,  1,  4,  9, 16])

10 矩阵运算

线性代数中,矩阵的相乘操作在NumPy 中怎么实现,两种方法:dot函数包装为matrix对象

dot 操作:

# 数值[1,10)内,生成shape为(5,2)的随机整数数组  v2 = random.randint(1,10,(5,2))  In [77]: v2  Out[77]:  array([[8, 6],         [9, 4],         [4, 6],         [1, 3],         [2, 1]])  # v1:1行5列,v2:5行2列,结果1行2列  In [78]: dot(v1,v2)  Out[78]: array([28, 29])

另一种方法包装为matrix类

In [81]: matrix(v1)*matrix(v2)  Out[81]: matrix([[28, 29]])

经过包装后,shape取值也会变得更符合线代中的常识:

In [83]: matrix(v1).shape  Out[83]: (1, 5)

而在没有包装前:

In [84]: v1.shape  Out[84]: (5,)

求矩阵的行列式,要求数组的最后两个维度相等,因此重新构造一个矩阵:

In [88]: v3 = random.randint(0,10,(3,3))    In [89]: v3  Out[89]:  array([[6, 5, 8],         [8, 4, 0],         [3, 8, 1]])    In [90]: v3m = matrix(v3)    In [91]: v3m  Out[91]:  matrix([[6, 5, 8],          [8, 4, 0],          [3, 8, 1]])    In [92]: linalg.det(v3m)  Out[92]: 399.9999999999999

11 统计变量

求平均值:

In [93]: v3  Out[93]:  array([[6, 5, 8],         [8, 4, 0],         [3, 8, 1]])    In [94]: v3.mean()  Out[94]: 4.777777777777778  In [95]: v3.mean(axis=1)  Out[95]: array([6.33333333, 4.        , 4.        ])

求标准差:

In [97]: v3.std()  Out[97]: 2.8588178511708016

求方差:

In [98]: v3.var()  Out[98]: 8.17283950617284

求最大值:

In [99]: v3.max(axis=1)  Out[99]: array([8, 8, 8])

求最小值:

In [106]: v3.min(axis=1)  Out[106]: array([5, 0, 1])

求和:

In [107]: v3.sum(axis=1)  Out[107]: array([19, 12, 12])

求累乘:

In [108]: v3.cumprod(axis=1)  Out[108]:  array([[  6,  30, 240],         [  8,  32,   0],         [  3,  24,  24]], dtype=int32)

求累和:

In [109]: v3.cumsum(axis=1)  Out[109]:  array([[ 6, 11, 19],         [ 8, 12, 12],         [ 3, 11, 12]], dtype=int32)

求迹:

In [111]: v3.trace()  Out[111]: 11    In [112]: diag(v3).sum()  Out[112]: 11

12 改变Shape

NumPy数组的shape 被修改而无需复制原有数据,这使它更为高效。

In [117]: v4 = v3.reshape((1,9))  In [118]: v4  Out[118]: array([[6, 5, 8, 8, 4, 0, 3, 8, 1]])

我们验证下v4 是否真的没有被复制:

# 修改v4的第一行第2列的元素5,为10  In [120]: v4[0,1]=10  In [121]: v4  Out[121]: array([[ 6, 10,  8,  8,  4,  0,  3,  8,  1]])  # 查看v3,发现对应元素也变为10  In [122]: v3  Out[122]:  array([[ 6, 10,  8],         [ 8,  4,  0],         [ 3,  8,  1]])

所以验证得出:v4仅仅是v3的视图,未发生复制。

NumPy 提供的flatten 函数也有改变shape 的能力,将高维数组变为向量。但是,值得注意的是它会发生数组复制行为,因此不是高效的

有时,我们需要增加维度,此时可使用newaxis,它会插入1个维度,如下在第三个维度插入,v5的shape变为: [3,3,1]

In [128]: v5 = v3[:,:,newaxis]  In [129]: v5  Out[129]:  array([[[ 6],          [10],          [ 8]],           [[ 8],          [ 4],          [ 0]],           [[ 3],          [ 8],          [ 1]]])

13 数组由小变大

NumPy中的函数 repeat, tile, vstack, hstack, 和concatenate 具备变换小数组为大数组的能力。

repeat复制元素

In [132]: a = array([[1,2],[3,4]])    In [137]: repeat(a,2,axis=1)  Out[137]:  array([[1, 1, 2, 2],         [3, 3, 4, 4]])    In [138]: repeat(a,2,axis=0)  Out[138]:  array([[1, 2],         [1, 2],         [3, 4],         [3, 4]])

tile 复制块:

In [5]: tile(a,2)  Out[5]:  array([[1, 2, 1, 2],         [3, 4, 3, 4]])    In [6]: tile(a,(2,3))  Out[6]:  array([[1, 2, 1, 2, 1, 2],         [3, 4, 3, 4, 3, 4],         [1, 2, 1, 2, 1, 2],         [3, 4, 3, 4, 3, 4]])

vstack竖直方向合并数组:

In [18]: c = array([[-1,-2]])  Out[20]:  array([[ 1,  2],         [ 3,  4],         [-1, -2]])

hstack 水平方向合并数组:

b = array([[40],[12]])  hstack((a,b))  array([[ 1,  2, 40],         [ 3,  4, 12]])

concatenate指定在哪个轴向上合作数组:

In [26]: concatenate((a,c),axis=0) # 效果等于vstack  Out[26]:  array([[ 1,  2],         [ 3,  4],         [-1, -2]])    In [27]: concatenate((a,b),axis=1) # 效果等于hstack  Out[27]:  array([[ 1,  2, 40],         [ 3,  4, 12]])

以上全部手码,利用两个周的课余时间整理出来,从十几个角度汇总NumPy平时经常使用的函数,希望对大家有用。