Python常用模块之numpy

时间:2022-11-12 22:23:36

numpy

在讨论numpy的具体函数和方法之前,我要先说明一下两个问题:1,numpy中的数组和向量。2,numpy中的“多轴数组”。

维度vs轴数

numpy中里有多维数组,为了避免和线性代数中的多维数组区别开,这里暂时称之为多轴数组

我们首先生成一个三维数组,里面存放数字0-36:


arr = np.arange(36).reshape(3, 4, 3)

我们可以将这个三轴数组看作一个立方体:每一个小立方体代表一个数字,这些数字按照一定的方式堆砌(排列组合)成一个3*4*3的立方体。三个轴分别为axis0, axis1, axis2。

那么这个reshape方法中的3个参数3,4,3到底指的是什么呢?其实,它们规定了:这个轴的方向上有几个维度

注意,我们说的numpy中的二维数组(比如是n阶的),实际上是线性代数中的n维数组,或者称为n阶方阵。我们在python里把它叫做二维数组,并不表明这个数组在线性空间中是2阶的,而是这个数组可以在二维平面中表示出来。比如我们举的这个例子,三轴数组就不能在二维平面中表现出来,但我们可以在三维空间里将它形象化。

有了上面的解释,我们便可以知道:axis0方向上有三个维度,axis1方向上有4个维度,axis2方向上有3个维度。

从axis0的方向上看过去,一共有3个维度,每一维都是4*3的”二轴数组“。

Python常用模块之numpy

从axis1的方向上看过去,一共有4个维度,每一维都是3*3的”二轴数组“。

Python常用模块之numpy

从axis2的方向上看过去,一共有3个维度,每一维都是3*4的”二轴数组“。

Python常用模块之numpy

好了,其实我们只要知道python中的多维数组,和线性代数中的多维数组完全不同就可以了。我们通常还是只使用二轴数组,只要不造成歧义,表述成“二维n阶数组”也是可以的。

numpy中按轴进行的操作有很多,我们将轴的概念理解好,会对矩阵操作带来很大的方便,接下来举几个例子:


import numpy as np

arr = np.arange(12).reshape(4, 3)
mean0 = np.mean(arr,axis=0) #在axis0方向(向下,列方向)上操作,即对每一列求均值
mean1 = np.mean(arr,axis=1) #每一行的均值
sum0 = np.sum(arr,axis=0) #每一列的和
sum1 = np.sum(arr,axis=1) #每一行的和
min0 = arr.min(0) #每一列的最小值
min1 = arr.min(1) #每一行的最小值
max0 = arr.max(0) #每一列的最大值
min1 = arr.max(1) #每一行的最大值

mean0
Out[8]: array([4.5, 5.5, 6.5])
mean1
Out[9]: array([ 1.,  4.,  7., 10.])
sum0
Out[12]: array([18, 22, 26])
sum1
Out[11]: array([ 3, 12, 21, 30])

我们定义了一个4*3的矩阵。axis0方向的长度是4,axis1方向的长度是3.从axis0的方向看过去矩阵是3个4维列向量,从axis1的方向看过去是4个3维的行向量。

所以,按axis=0操作时,都是按照(axis1个)列向量操作的;按axis=1操作时都是按照(axis0个)列向量操作的;

我们按照axis0来求每个列向量平均数,得到了一个3阶数组;按照axis1来求每个行向量平均数,得到了一个4阶数组。

我们已经将arr reshape成了一个4*3的矩阵,axis0对应的轴上应该有四个维度(4行),按axis0轴求和就是将这四个维度(中的4个数据)求和,之后再按axis1的方向移动求另外三个数据的和,一共移动3次。

当然也可以简记为:axis=0是按列,axis=1是按行操作。

向量vs数组

我们用shape方法来获取一个矩阵的形状,它会会返回一个元组,还会用到T方法来进行矩阵转置。举一个简单的例子:


arr1 = np.arange(36)

arr1.shape
Out[14]: (36,)

arr1.T.shape
Out[15]: (36,)

对于arr1这个数组,转置没有效果。这说明我们有一个错误的观念:多维数组就是矩阵,一维数组就是列向量。这句话的前半句没有任何问题,但一维数组只是“一组有顺序的数”,并不是向量。你瞧它的形状是(36,),而非(36,1)。欲将其变为列向量或者行向量也简单,需要用到numpy中的expand_dims方法,需要给他两个参数,第二个是给定的轴(在这个轴的方向上增加维度)。也可以直接用matrix生成矩阵。


arr2 = np.expand_dims(arr1, axis=0) #朝着axis0的方向,即行
arr3 = np.expand_dims(arr1, axis=1) #朝着axis1的方向,即列
arr4 = np.matrix([1, 2, 3])
arr5 = np.matrix([1, 2, 3]).T

arr2.shape
Out[17]: (1, 36) #行向量
arr3.shape
Out[18]: (36, 1) #列向量
arr4.shape
Out[19]: (1, 3)  #行向量
arr5.shape
Out[20]: (3, 1)  #列向量

除了expand_dims方法外,还有其他的构造向量的方法,我们用下面的例子做一个测试,探索一下array函数构造矩阵的原理:


test1 = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8])
test2 = np.array([[0, 1, 2, 3, 4, 5, 6, 7, 8]])

test1.shape
Out[25]: (9,)
test2.shape
Out[26]: (1, 9)

可以看出test1的方式是构造了一个数组,而test2的方式是构造了一个行向量。分析一下array构造矩阵的原理:


np.array([[#, #, #],
  [#, #, #],
  [#, #, #]])

array方法中,传入的参数是一个列表。这个列表中存在n个子列表,便构造n行的矩阵,每个子列表中有m列元素就构造m列的矩阵。所以array的维度是由传入的列表中,子列表的个数n,和子列表中的元素个数m共同确定的。

那么特例就是:子列表的个数为1,相当于构造了一个1行的矩阵,即行向量。子列表有多个,但每个子列表中元素个数为1,那么这就是一个列向量咯。

数乘vs点乘


arr1 = np.array([[1, 2, 3],
                 [4, 5, 6],
                 [7, 8, 9]])
arr2 = np.arange(1, 10).reshape((3, 3))
arr3 = np.eye(3)
arr4 = np.matrix([1,2,3]).T

arr1 + 10
Out[65]:
[[11 12 13]
 [14 15 16]
 [17 18 19]]

arr1 + arr4
Out[77]: 
matrix([[ 2,  3,  4],
        [ 6,  7,  8],
        [10, 11, 12]])
 
arr1*arr3
Out[66]:
[[1. 0. 0.]
 [0. 5. 0.]
 [0. 0. 9.]]
 
arr1.dot(arr3) #等同于np.dot(arr1,arr3)
Out[67]:
[[1. 2. 3.]
 [4. 5. 6.]
 [7. 8. 9.]]
 
2**arr1
Out[68]:
[[  2   4   8]
 [ 16  32  64]
 [128 256 512]]
 
arr1**2
Out[69]:
[[ 1  4  9]
 [16 25 36]
 [49 64 81]]

数与矩阵相加,即与矩阵每一个元素相加。列向量与矩阵相加,即列向量与每一列相加。

np.dot(arr1,arr3)进行的是常规的矩阵乘法,两个矩阵的形状必须遵循“前列等后行“的规则。而arr1*arr3就是对应位置的元素作乘积,需要两个矩阵的形状相同。

切片vs拷贝

我们之间已经知道列表的切片就等于浅拷贝。那么对于数组也一样吗。


myarr16 = myarr[1]
myarr17 = myarr[1, 2]
myarr18 = myarr[1][2]
myarr19 = myarr[:, ::-1]
myarr20 = myarr[::-1, ::-1] 
myarr21 = myarr[::-1].copy()

np.may_share_memory(myarr19, myarr)
Out[46]: True
np.may_share_memory(myarr20, myarr)
Out[47]: True
np.may_share_memory(myarr21, myarr)
Out[48]: False

数组的切片有两种方法,如myarr17和myarr18。为了和列表切片区分开来,习惯于用myarr18的构造方式。

用numpy中的方法may_share_memory来查看,两个数组是否共享内存,也即是否为新数组创了新内存。可以发现切片始终与原数组共享内存,而copy方法进行了内存的创建,赋值。

高级切片

array的切片的元素可以是一个列表,表示抽取除这些行,或列。


myarr22 = np.random.randint(0, 20 ,10).reshape(2, 5)
myarr23 = myarr22[:, [4, 2]] 

myarr23
Out[54]: 
array([[17,  4],
       [ 1, 15]])

myarr22是一个2*5的矩阵,我们得到的myarr23是抽取出每一行的第5列,和第3列组成的新数组。

重新排序


myarr19 = myarr[:, ::-1]
myarr20 = myarr[::-1, ::-1] 
myarr21 = myarr[::-1].copy()

其实我们利用切片就可以对数组进行逆序排列。

myarr19进行列逆序排列, 第一个:不能少。因为np是默认按行进行操作的,如果只给一个参数,如myarr21会将该切片操作应用到每一行上,即将myarr的行之间逆序。

数组没有列表的原地排序的sort方法。


array0 = np.array([[9, 8, 7, 6],
    [6, 5, 4, 3],
    [3, 2, 1, 0]])
array1 = np.sort(array0, axis=0) #朝着axis0的维度,对每一个列向量排序
array2 = np.argsort(array0, axis=0)
array3 = np.sort(array0, axis=1) #朝着axis1的维度,对每一个行向量排序
array4 = np.argsort(array0, axis=1) 

array1
Out[50]: 
array([[3, 2, 1, 0],
       [6, 5, 4, 3],
       [9, 8, 7, 6]])

array2
Out[51]: 
array([[2, 2, 2, 2],
       [1, 1, 1, 1],
       [0, 0, 0, 0]], dtype=int64)

array3
Out[52]: 
array([[6, 7, 8, 9],
       [3, 4, 5, 6],
       [0, 1, 2, 3]])

array4
Out[53]: 
array([[3, 2, 1, 0],
       [3, 2, 1, 0],
       [3, 2, 1, 0]], dtype=int64)

array2是按列排序返回顺序列表,返回矩阵中的列向量表示:原矩阵中对应列向量从小到大排列之后,对应元素在原列表中的位置。也即是array1中列向量的元素在array0中对应列向量的索引。

array4是按行排序返回顺序列表,返回矩阵中的行向量表示:原矩阵中对应行向量从小到大排列之后,对应元素在原列表中的位置。也即是array3中列向量的元素在array0中对应行向量的索引。

等差数列

等差数列有两种生成方法:


myarr3 = np.arange(1, 10, 3)
myarr4 = np.linspace(9, 10, 5)
myarr5 = np.linspace(9, 10, 5, endpoint = False)

myarr3
Out[41]: array([1, 4, 7])
myarr4
Out[42]: array([9. , 9.25, 9.5, 9.75, 10.])
myarr5
Out[43]: array([9. , 9.2, 9.4, 9.6, 9.8])

arange(start, end(exclusive), step) 三个参数,前两个参数表示生成等差数列的区间,左闭右开。第三个参数为步长缺省值为1。

linspace(start, end, number_point) 将区间切分为number_point-1份。其实linspace还有一个默认参数 endpoint = True,缺省时置True,即左闭右闭的闭区间。np.linspace(start, end, number_point, endpoint = False) 将区间切分为number_point份,因为endpoint = False,所以与arange一样是左闭右开。

特殊矩阵

全零矩阵,全一矩阵,单位阵,对角阵,随机矩阵的创建方法:


myarr6 = np.ones((3, 4))
myarr7 = np.zeros((3, 4))
myarr8 = np.eye(4)  #eye的参数不能是远足,只能是一个数字
myarr9 = np.diag([1, 2, 3, 4])
myarr10 = np.random.rand(4, 2)

其中,ones和zeros的参数都只能是来描述矩阵的形状的元组。eye的参数只能是一个int类型数据。diag的参数一个存放着特征值的列表

另外提一个空矩阵,只分配地址,但不存入数据

矩阵扩展

我们可以用numpy中的tile函数对数组朝指定方向进行扩展指定次数:


mylist = [7, 999, 4]
array1 = np.tile(mylist, (1, 3))
array2 = np.tile(mylist, (3, 2))
array3 = np.tile(mylist, 2)

array1
Out[55]: array([[  7, 999,   4,   7, 999,   4,   7, 999,   4]])

array2
Out[56]: 
array([[  7, 999,   4,   7, 999,   4],
       [  7, 999,   4,   7, 999,   4],
       [  7, 999,   4,   7, 999,   4]])

array3
Out[57]: array([  7, 999,   4,   7, 999,   4])

tile(mylist, (m, n))相当于构造了一个m*n的矩阵,并将里面的每个元素填充维mylist。

hstack函数,和r_函数都是在axis0方向上合并,即合并在右边。vstack函数,和c_函数都是在axis1方向上合并,即合并在下边。


myarr1 = np.array([[ 8.,  5.],
          [ 1.,  6.]])
myarr2 = np.array([[ 1.,  9.],
          [ 8.,  5.]])

np.hstack((myarr1, myarr2))
Out[69]: 
array([[8., 5., 1., 9.],
       [1., 6., 8., 5.]])

np.c_[myarr1, myarr2]
Out[73]: 
array([[8., 5., 1., 9.],
       [1., 6., 8., 5.]])
       
np.vstack((myarr1, myarr2))
array([[8., 5.],
       [1., 6.],
       [1., 9.],
       [8., 5.]])
       
np.r_[myarr1, myarr2]
Out[72]: 
array([[8., 5.],
       [1., 6.],
       [1., 9.],
       [8., 5.]])

数据类型


myarr12 = np.array([1, 2, 3])
myarr13 = np.array([1, 2, 3, 4], dtype = 'float')
myarr14 = np.array([1., 2., 3.])
myarr15 = np.array([1+1j, 2+2j, 3+3*1j])    #numpy中的复数符号是1j,而不是j

myarr12是int32型的数组,myarr13,myarr14都是float64型的数组。我们当然也可以构造复数complex128类型的数组,比如myarr15。

矩阵大小


arr0 = np.arange(0, 12).reshape(4, 3)

arr0.size
Out[65]: 12
arr0.shape
Out[66]: (4, 3)
arr0.shape[0]
Out[67]: 4
arr0.shape[1]
Out[68]: 3
arr0.dtype.itemsize
Out[69]: 4

生成一个4*3的矩阵,size方法获得其中元素的个数,返回一个int数据。shape方法返回一个元组(num,dim),第一个元素为行数,第二个元素为列数。

dtype.itemsize是numpy中的方法,获取数组所占用的字节数。