天天看点

【Python】 Numpy极简寻路

【Numpy】

  先感叹下最近挖坑越来越多了。。

  最近想不自量力地挑战下ML甚至DL。然而我也知道对于我这种半路出家,大学数学也只学了两个学期,只学了点最基本的高数还都忘光了的渣滓来说,难度估计有点大。。总之尽力而为吧。在正式接触ML的算法之前,Numpy是一个必须知道的Python库。其中有很多关于线代的类和方法可以直接用。

  当然Numpy不是内建的库,但是pip install numpy一下也很简单。

  ■  方法罗列

  我也不知道怎么开始写好,按书上的教程,罗列下提到的方法吧。。书上代码一个大前提是from numpy import *。但是有点Python经验的人都知道,import *不是一个很好的引入办法。所以我还是把模块名给老实写出来。

  numpy.random  这还是个小模块,类似python内建的random模块,里面涵盖了很多用于随机生成一些数据的方法如random,randint,choice等等。

  numpy.random.rand(a,b)  这个rand方法是不太熟悉的,其作用是生成一个a * b的二维数组,数组中的每一个元素都是随机出来的。虽然两个循环或者 嵌套列表表达式也能做这个事,不过一个方法就搞定更快。

  numpy.mat  此方法就是将某个二维数组转化成一个矩阵。由于矩阵的类Matrix类的__str__方法是返回二维数组的形式,所以print的时候看起来和rand出来的东西没差别,但是实际上是个矩阵对象。

  矩阵对象matrix的I(大写的i)属性是其逆矩阵。通过*符号将两者相乘即可得到单位矩阵:

>>> nest_list = numpy.random.rand(3,3)
>>> matrix = numpy.mat(nest_list)
>>> matrix
matrix([[ 0.31934803,  0.65214401,  0.78380241],
        [ 0.00338375,  0.64812103,  0.19773746],
        [ 0.08785176,  0.04199491,  0.13058765]])
>>> invMat = matrix.I
>>> invMat
matrix([[ -8.38826956,   5.74139174,  41.65369116],
        [ -1.86042236,   2.98414596,   6.64784212],
        [  6.24142078,  -4.82212736, -22.50232256]])
>>> invMat * matrix
matrix([[  1.00000000e+00,  -1.55431223e-15,  -8.88178420e-16],
        [  0.00000000e+00,   1.00000000e+00,  -2.22044605e-16],
        [  0.00000000e+00,   6.66133815e-16,   1.00000000e+00]])      

  相乘之后得到的单位矩阵中,除了对角线外其余部分不是0的原因是因为处理浮点数的误差。

  numpy.eye(n)  eye方法可以直接生成n阶的单位矩阵。比如上面那个矩阵直接减去eye(3)就会变成空矩阵了。

  numpy.linspace(a,b,n)  abn三个参数都是实数且n是整数,这个函数返回一个array对象,其内容是将[a,b]范围按照n-1等分,然后各个等分的节点上的数(统一为float型)填充进array中。由于两端都闭合,所以返回列表的长度是n。比如linspace(0,1,10)返回的是[0., 0.1111111, 0.22222 ..... 0.999999, 1.]

  numpy.arange(a,b,s)  和linspace很像,只不过这个函数,是从a开始(包括a)逐渐一个个加上s,并把每加上一个s之后的值充入array,直到值大于等于b时停止。需要注意区间右端开,即如果a + k*s之后刚好等于b,那么b是不被加入这个array的。

■  array类

  numpy.array是整个numpy中最为重要的类型之一。相比于Python中自带的数组类型(列表),numpy中的array做了很多利于数学计算的封装。

  比如对于 array对象a 来说,有如下属性可以使用:

a.T             a.choose        a.data          a.flatten       a.nbytes        a.repeat        a.sort          a.tostring
a.all           a.clip          a.diagonal      a.getfield      a.ndim          a.reshape       a.squeeze       a.trace
a.any           a.compress      a.dot           a.imag          a.newbyteorder  a.resize        a.std           a.transpose
a.argmax        a.conj          a.dtype         a.item          a.nonzero       a.round         a.strides       a.var
a.argmin        a.conjugate     a.dump          a.itemset       a.prod          a.searchsorted  a.sum           a.view
a.argsort       a.copy          a.dumps         a.itemsize      a.ptp           a.setfield      a.swapaxes
a.astype        a.ctypes        a.fill          a.max           a.put           a.setflags      a.take
a.base          a.cumprod       a.flags         a.mean          a.ravel         a.shape         a.tofile
a.byteswap      a.cumsum        a.flat          a.min           a.real          a.size          a.tolist              

  首先需要说明的是,这里的数组是广义上的数组。即可以不止一维。换句话说,嵌套多层的数组也就是n维数组也可以使用这里的方法。

  其次,array对象和多层嵌套的列表是不同的。比如对于array对象的加减乘除等操作就不同。对于普通列表[1,2,3]和array([1,2,3])而言。前者如果令 ls * 2,得到的是[1,2,3,1,2,3],而后者得到的是[2,4,6]。更牛的是后者还可以做2 * ls + 1形成[3,5,7]。也就是说,默认的加减乘除对于array对象而言其实是对于数组中所有成员数的直接操作求值。

  关于数组的维度: 对于一个最小组成单位统一(通常就是整数,这个和上面属性中的dtype有关),从代码来说只要看数组的表达式从最开头开始,到第一个最小组成单位为止,之间有几个中括号,那就是几维数组。人类大脑比较容易接受的有一维数组[1,2,3],或者二维数组[[0,1,2],[1,2,3]]。前者不用说,后者的话可以理解成一些坐标系内点的集合(不过也就到三维坐标系为止,以上维度的很难想象)。复杂的高维数组,简单来说可以理解成组成数组的元素是小数组,而小数组中又有小小数组这样子。

  对于高维数组的取值,当然可以用a[x][y]这样的方式来取值,但是中括号接中括号很不好看。array类实现了中括号中多参数的办法来取值。比如a[x,y]就可以了。甚至可以有a[x,:]这种切片也放在这里。这些还都是类似于Python列表基于行的取值。如果将切片放在第一个参数甚至可以做到整个数组基于列的取值,比如a[:,1],就可以取到所有行的第一列的数据(当然举例时想成二维数组即可。)

  简单说明一下其中的几个(方法后面会带上括号,没有括号的就说明是属性)

  a.all()和a.any()是判断数组的黑白情况。所谓黑白,就是将整个数组flatten成一个一维的数组,然后查看组成其的基本元素是否全是真或者至少有一个真(对于int型,真就是指!=0)。如果全是真,a.all()返回True,否则返回False。如果至少有一个是真a.any()返回True,否则False

  a.max()和min()就是把a给flatten之后获取最大、最小值。相对应的argmax()和argmin()返回的是最大值和最小值所在位置的下标。

  a.size会返回数组中最小组成单位的总个数。注意a.size和len(a)在高于一维的情况下明显是不等的。len(a)计算得到的是第一维上得到的长度。

  a.dtype会返回组成此数组的最小单位的基本类型。如果全是数字则返回int(64),如果是类似于[[1,2],3]这样不规则的混合类型,则返回object

  a.ndim  返回数组的维数。numpy中数组的维数并不要求一定要有具体内容。比如array([])的ndim就是1,而array([[]])就是2

  a.shape  返回一个元组,内容是数组在各个维度上的长度。这个比较拗口了,对于一个规则的可以形成矩阵的数组,其特定一个维度上各个元素的长度应该是相同的。所以我们可以得到一个统一的shape。比如对于下面这几个数组的shape返回值,体会一下:

[1,2,3]
# (3,)

[[0,1,2],[1,2,3]]
# (2,3)
# 从最外面往里面走,第一层(第一维)长度是2,由两个元素组成。恰好这俩元素都是数组,说明有第二维,继续往里走
# 第二维的长度是3,所以最终数组在各个维度上的长度用shape体现出来就是(2,3)

[
  [
    [1,2,3],
    [4,5,6]
  ],
  [
    [7,8,9],
    [10,11,12]
  ]
]
# 同上理,这个三维数组的shape是(2,2,3)      

  可以知道,a.shape这个元组的长度等于a.ndim,各个元素之积等于a.size。

  a.reshape(*args)  可以向reshape方法传递一些数字,只要这些数字的积是a.size,那么就返回一个shape为指定那些数字的一个数组。a可以不是一个flatten的数组,同时这个方法是返回我说的那样的一个新数组而不是在a本身做出操作。

  a.flatten()  将高维数组降成线性的。同样是返回一个新数组而不是在a本身操作。

  a.sum(axis)  首先要知道什么是数组间的求和。不同于[1,2] + [3,4]得到的是[1,2,3,4],array([1,2]) + array([3,4])得到的是array([4,6])。顺便,array([1,2]) + array([3,]) = array([4,5])。现在重点关注前面这种规则的相加形式。axis参数可以是一个数字,它指出了我们要合并数组的第几维,axis具体数值对应的是shape元组的下标。例如对于数组a = array([[1,2],[3,4],[5,6]]),有a.shape == (3,2),所以axis可以是0或者1,分别对应a的第一维和第二维。根据shape,第一维的长度是3,第二维的长度是2。在做了sum操作之后,返回的内容应该是axis指定的那个维度被合并之后的情况。比如指定axis=0时,最终应该返回一个shape是(2,)的数组;若axis=1时,返回一个最终shape为(3,)的数组。

  那么具体怎么操作呢?以前者的情况为例,合并第一维,指的是将第一维上各元素相加。第一维上的元素分别是[1,2]和[3,4]和[5,6]这三个数组。根据数组相加规则,最终得到的就是[1+3+5, 2+4+6]即[9,12]。这个数组刚好shape是(2,)符合我们的预期。同理,后者是要合并第二维,所以要在第二维的层面上看,第二维的层面就是[1,2]中的1和2,以及[3,4]中的3和4……。将它们分别相加,得到的是[1+2,3+4,5+6]即[3,7,11],这个的shape也刚好是(3,)。

  以上是axis参数为单纯一个数字的情况,其实还可以以元组的形式同时指定多个维度要合并。比如这个例子中指定axis=(0,1),那么最终数组被合并成21这一个数字。当不指定axis参数时默认就是合并成零维数组,即一个数字。换句话说,a.sum()其实就是a.sum(tuple(range(len(a.shape))))。

  tile(A,reps)  tile是numpy.tile,一个独立的方法。其第一个参数是一个若干维的数组(包括零维),第二个是一个合法表示的shape量。正常情况下,tile的操作是将A的第n维重复reps[n-1]次。如果当前维度还没有到达最底层,那么就是数组层面的重复;如果已经到达最底层,那么就是做了类似于lst.append(*lst)这样的操作。例子:

a = array([[1,2],[3,4],[5,6]])
tile(a,(2,1))
'''
得到结果
array([[1, 2],
       [3, 4],
       [5, 6],
       [1, 2],
       [3, 4],
       [5, 6]])
reps=(2,1)之意为第一维上*2即再重复一遍,第一维原数据是三个小数组组成的数组,没有到达底层,所以数组层面上重复,得到结果第一维的长度从原先3变成了6
第二维reps是1,即*1,即不变
'''
tile(a,(1,2))
'''
array([[1, 2, 1, 2],
       [3, 4, 3, 4],
       [5, 6, 5, 6]])
如果改成第二维*2,由于第二维已经是底层,所以原来元素直接重复一遍,而不是变成
[[1,2],[1,2]],[[3,4],[3,4]]... 这样维度上升了!
'''      

   上面的情况,reps的长度刚好和A的维数一样。如果不一样呢。设reps长度为D,a的维数是a.ndim。如果D>a.ndim,就在a外面升维,升的每个维度长度都是1。升到A的维数和D相等再做tile操作;

  若D<a.ndim,就在D前面扩充1,比如(2,2)将被扩充为(1,1,1...2,2)使得D等于a.ndim,再做tile操作。换句话说,D不够长时,指出的需要进行重复操作的维度默认从最底层开始算。

  zeros  zeros也是numpy.zeros,其参数可以是一个合法的shape量,用来生成指定shape的array,其中最小单位元素由数字0填充。默认这个0是float即0.0,可以dtype参数指定int等改变其类型。

  a.argsort()  通过一个array对象a调用,使用后返回另一个array,内容是按照从小到大顺序排列a后,各个元素在原来a中的下标。比如array([4,1,3,2]).argsort()返回的是array([1,3,2,0])

  may_share_memory(a,b)  在Python中原生的切片功能中,默认切片后会创建出一个新的对象来保存数据。但是array对象其实是对一个序列对象的部分引用,因此有可能出现array切片后不创建一个新对象。比如a = array([1,2,3]),b = a[1:],b[1] = 5。这里把a切片出一部分赋值给了b,看似b和a就独立开来了。但是在对b做了一些改变之后a也会随之被改变。即a现在是array([1,2,5])。而may_share_memory就是numpy留出的一个判断两个array中是否有同指向引用的接口。它返回一个True或者False。

  a.copy()  copy方法就是用来解决上面说的这个问题的。当然你也可以用Python自带的copy模块的deepcopy方法。

  numpy.dot(a,b)  刚才也说了,array对象的乘法是各个对应位置分别相乘,而不是矩阵乘法。如果要矩阵乘法,一个办法是转化array为mat对象后相乘。另一个就是使用dot函数相乘两个array。

  ● 总结一下

  本身运算的特殊性:array类对象很好地直接和运算符号结合。如两个shape一样的array类对象a和b,a+b,a-b,a*b,a/b都是直接将各个对应位置的元素进行相应的计算。a % 2 == 0返回的是一个全部都是True,False,但是shape和a一样的array。取True还是False就是看相关位置的元素%2是不是0了。

  进行array的乘除时,如果两者的shape不匹配,也不一定就是不能计算。numpy中会有一种“广播机制”(boardcast),如果在一个或多个维度方向上重复运算对象中比较小的array,使得比较小的array能够刚好覆盖掉比较大的array,一个元素都不多不少,此时numpy就会默认这样做。比如a = array([[1,1],[2,2],[3,3]])加上了b = array([1,1]),那么得出结果应该是array([[2,2],[3,3],[4,4]])

  切片:array的切片比Python自带的序列切片更加高级。除了a[1:],a[:2],a[::3]等等切片方式之外,还支持

  如a[2,3:5]之类的tuple作为判别式的切片(所有条件都写在一个中括号中,不用再去写两个中括号了)

  如a[a>=10]这样的“逻辑切片”,这类需要注意中括号中的a必须和数组名保持一致。

  如a[[2,4,5]]这样的离散式地选择一些值。

  关于形态:a.shape, a.size, a.ndim  这些是属性。a.reshape(*args),  a.flatten() 这些是方法。

  考察数组中内容:a.all(),a.any()    a.max(), a.argmax(axis), a.min(), a.argmin(axis)    a.sum(axis)    a.dtype    a.argsort()

  numpy直属函数:tile(element, axis)    zeros/ones()

  numpy中集成的一些类似其他模块的函数: sqrt/cos/sin/exp()    arange()    a.copy()   

  numpy.random中也有很多和random中类似的方法。比较常用的有numpy.random.random_intergers(a,b, (c,d))用来生成一个shape为(c,d)的array。每个元素都从[a,b]范围中随机选一个值。

  

  ●  关于axis参数与降维

  上面提到过array调用sum方法的时候可以传入axis参数。实际上在其他的一些方法中比如min,max中,axis参数也有很广泛的运用。下面对axis参数的意义再做一次解释说明。

  可以感觉得到,axis的降维操作应该是一种成体系的数学上的操作,不过我还没学到过… 只能从经验主义的角度来总结一下axis 的用法。

  首先我们可以通过Python代码中比较司空见惯的高维数组的写法中确定这个数组的各个维的长度。具体的,我们要确定每一维的元素个数有几个。如下图所示:

【Python】 Numpy极简寻路

  很明显,图中的这个array是一个三维的数组,而每个维度的长度就是各个维度的一个单位元素中有多少个次维度元素,即可以画多少个箭头。第一维这个列表中包含两个子列表,第二维也是,第三维单位元素则包含三个数字,所以这个数组a的shape是(2,2,3)。

  然后当我们调用sum,或者max,min之类的方法,指定axis参数时,axis参数对应的是shape的一个(或者若干个,这里先以一个为例)下标。它最直接的含义是,这个方法返回的array的shape,应该是原array的shape去掉相应下标后的值。如上面的a,如果调用了sum(axis=1),那么方法返回的应该是一个shape是(2,3)的array;如果是sum(axis=2),那么返回的应该是一个shape是(2,2)的array。

  仅仅知道shape是不够的,那么怎么构造这样一个降维后的array,知道其具体的值呢?一个直观的方法是,哪个维度被指定为axis了,就是指这个维度对应的那些箭头在一个“父元素”内只能存在一个了。比如sum(axis=1),此时第二维被指定为axis。第二维元素对应的是绿色的箭头,目前有两个绿色的箭头。那么怎么样才能只保留一个绿色的箭头呢,就是将两个绿色的箭头指向的元素上下相加(array互相相加相当于各个元素互相相加),因此我们得到的是一个(2,3)的二维数组是这样的:[[1,3,5],[4,2,2]]。

  类似的sum(axis=2)是将红色的箭头统统合并,所以相加是横向进行的。

  如果换个方法,max和min中指定axis,其实要义也是只保留一个同色箭头,但是这里保留的具体方法就不是加和所有同色箭头指向的元素,而是找出同色箭头指向元素中的最大/最小值了。如箭头指向的元素不是一个数字值,而是一个数组甚至高维数组呢?也很简单,就是比较这些元素相同位置的数字值,只选取其中的最大/最小值即可。比如max(axis=0)时,两个蓝色箭头指向的元素都是个二维数组,那么就比较二维数组中各个位置的值并取其大者。最终呈现出来的东西,就是[[3,1,4],[1,2,1]]

■  numpy中提供的一些多项式操作以及和线性代数相关的一些内容

  np.poly1d(list)  可以通过numpy隐性地构造一个多项式。比如np.poly1d([1,-4,3])就代表了x^2 - 4x + 3这个多项式。如果令p = np.poly1d([1,-4,3]),可以进行如下运算

  p(0)  代入x = 0时多项式的值

  p.roots  求出多项式等于0时这个方程的根

  p.order  多项式的阶数

  p.coeffs  多项式的系数,即上面给出的list

  np.polyfit(x,y,degree)  这个函数可以用于将(x, y)这组数据组成的点拟合成一个多项式,多项式的最高次数可以通过degree指定。然后这个函数返回的东西是一个系数的列表,将其放在poly1d()里面就可以构造出这个多项式对象。然后调用p(n)就可以大概估计x为n的时候y的值啦。*x不能太离谱,就好比ML一样,如果用于训练的数据是0,1范围内,但是n突然给了个10,那计算出来的肯定不是很准的。

 https://www.yiibai.com/numpy

上一篇: 逆推思维