天天看點

【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

上一篇: 逆推思維