NumPy 是 Python 中一個(gè)基本的科學(xué)計(jì)算庫(kù),包含以下特性:
其中,N 維數(shù)組是 NumPy 最為核心的特性。
除了顯而易見的科學(xué)計(jì)算用途,NumPy 還可以用作一般數(shù)據(jù)類型的多維容器,并且是任何數(shù)據(jù)類型均可;但有一點(diǎn):一個(gè)數(shù)組中必須是同種數(shù)據(jù)類型。這一特性使得 NumPy 可以高效、無(wú)縫地與各種數(shù)據(jù)庫(kù)進(jìn)行集成。
NumPy 基于開源許可協(xié)議 BSD license 發(fā)布,對(duì)于二次分發(fā)使用幾乎沒有限制。
由于 NumPy 并非 Python 的內(nèi)置模塊,因此我們直接從 Python 官網(wǎng)下載安裝的發(fā)行版是不包含 NumPy 這個(gè)模塊的。這個(gè)時(shí)候你要是想import numpy
,顯然是會(huì)無(wú)功而返的。因此我們需要額外安裝 NumPy 模塊。
安裝 NumPy 有好幾種方式,我們這里推薦的是:1)使用pip
進(jìn)行安裝;2)安裝Anaconda。
pip
安裝這種方式推薦給已經(jīng)從 Python 官網(wǎng)下載了某個(gè) Python 發(fā)行版的讀者,或是已經(jīng)通過其它方式獲得了 Python 環(huán)境,但卻沒有 NumPy 這個(gè)模塊的讀者。
安裝命令:
pip install numpy
或:
python -m pip install numpy
均可。
當(dāng)然,實(shí)際上 NumPy 模塊本身也有很多依賴,也需要其他一些模塊才能夠真正發(fā)揮出它強(qiáng)大的功能,因此我們推薦一次安裝多個(gè)模塊:
python -m pip install --user numpy scipy matplotlib ipython jupyter pandas sympy nose
這種方式適合還沒有安裝 Python 的讀者,或是已經(jīng)安裝了 Python 但是想一勞永逸擁有大多數(shù)科學(xué)計(jì)算庫(kù)的讀者。
訪問 Anaconda 官網(wǎng)找到下載鏈接進(jìn)行安裝即可。
或者如果你覺得 Anaconda 過于臃腫,也可以安裝其簡(jiǎn)化版本 Miniconda 。
N 維數(shù)組,也稱“多維數(shù)組”,這里的“N”代表的就是數(shù)組的維數(shù)。
我們?cè)谥耙呀?jīng)學(xué)過了 Python 中的內(nèi)置數(shù)據(jù)類型,其中有一種在表現(xiàn)形式上跟多維數(shù)組很像——就是序列(sequence)型的列表(list)數(shù)據(jù):二者都是由中括號(hào)括起來(lái)的數(shù)據(jù)類型。
但實(shí)際上,多維數(shù)組和列表是完全不同的兩種數(shù)據(jù)類型,我們?cè)谑褂玫臅r(shí)候一定要嚴(yán)格地將二者區(qū)分開來(lái)。具體地講,二者有以下不同點(diǎn):
本小節(jié)代碼示例來(lái)自《何為 NumPy》
讓我們來(lái)看一個(gè)例子(暫時(shí)忽略對(duì)變量的定義,假定存在 a、b、result 這三個(gè)數(shù)組)。
假設(shè)我們現(xiàn)在有 a、b 兩個(gè)一維數(shù)組(類似于一維列表),要對(duì)它們進(jìn)行“對(duì)應(yīng)元素相乘”的運(yùn)算,如果使用 Python 內(nèi)置的列表,應(yīng)該寫成什么樣呢?
result = []
for i in range(len(a)):
result.append(a[i]*b[i])
顯然結(jié)果是正確無(wú)誤的。但是我們考慮一個(gè)情形:如果 a、b 兩個(gè)一維數(shù)組中每個(gè)都包含數(shù)百萬(wàn)甚至更多的元素,會(huì)發(fā)生什么呢?眾所周知,Python 作為一門解釋型語(yǔ)言,其代碼的執(zhí)行速度本就遠(yuǎn)低于眾多的編譯型語(yǔ)言,并且之前我們也講到過,Python 中的for
循環(huán)實(shí)際上是一個(gè)迭代計(jì)算的過程,這樣就會(huì)導(dǎo)致性能上成本很高;大多數(shù)時(shí)間都被浪費(fèi)在了對(duì)代碼的解釋和對(duì)對(duì)象的操作上。
講到性能問題,我們一下就想到了日常被拉出來(lái)和 Python 比較的 C 語(yǔ)言。那么我們用 C 語(yǔ)言來(lái)實(shí)現(xiàn)上面這個(gè)運(yùn)算又如何呢?
for (i = 0; i < rows; i++): {
result[i] = a[i]*b[i];
}
較之之前的 Python 代碼,顯然這段 C 代碼節(jié)省了解釋代碼和操作對(duì)象的性能開銷,但凡事有利有弊,提升了性能的同時(shí)也犧牲了 Python 代碼的簡(jiǎn)潔明了。如果是二維數(shù)組呢?
for (i = 0; i < rows; i++): {
for (j = 0; j < columns; j++): {
result[i][j] = a[i][j]*b[i][j];
}
}
好了,不用再往上加了。我們完全可以想見,隨著數(shù)組維數(shù)越來(lái)越多,這段代碼嵌套的循環(huán)也將越來(lái)越多,代碼越來(lái)越龐雜——總之直觀性越來(lái)越差。
如果用上 NumPy 呢?同樣是計(jì)算兩個(gè)數(shù)組的逐元素乘積,NumPy 對(duì)于包含多維數(shù)組的運(yùn)算默認(rèn)模式就是“逐元素運(yùn)算”,同時(shí)又通過預(yù)編譯好的 C 代碼來(lái)實(shí)現(xiàn)高性能的計(jì)算。使用示例如下:
result = a * b
啊哈!是不是有點(diǎn)眼熟?這不就是一個(gè)很自然的數(shù)學(xué)表達(dá)式嗎?當(dāng)然跟矩陣的點(diǎn)乘還是不一樣的,但是只要了解 NumPy 的這個(gè)運(yùn)算規(guī)則的人,看到這行代碼就有一種一目了然之感。管你三七二十,兩維、十維還是八十、八百維,我都用這一個(gè)表達(dá)式全部搞定。既保留了 Python 代碼簡(jiǎn)潔明了的風(fēng)格,也獲得了較高的執(zhí)行速度。
在上面的例子中,支撐起 NumPy 強(qiáng)大功能的主要有兩個(gè)很重要的特性:矢量化(vectorization)和廣播(broadcasting)。
其中,矢量化描述的是這么個(gè)情形:凡是在遇到需要顯式循環(huán)、索引的地方,都可以省略掉這些細(xì)枝末節(jié),由預(yù)編譯好的、優(yōu)化過的 C 代碼在后臺(tái)默默耕耘。這么一來(lái)就帶來(lái)了幾個(gè)好處:
for
循環(huán)。“廣播”這個(gè)特性對(duì)于掌握 NumPy 非常重要,文章后面會(huì)單獨(dú)講解一下,希望讀者能夠用心理解
“廣播”是一個(gè) NumPy 的術(shù)語(yǔ),描述的則是這么一個(gè)情況:NumPy 中的運(yùn)算都是默認(rèn)逐元素進(jìn)行的。這些運(yùn)算不僅限于常見的算術(shù)運(yùn)算,也包括不那么常見的邏輯運(yùn)算、位運(yùn)算,以及函數(shù),等等,凡是用到這些運(yùn)算,或者說(shuō)操作,NumPy 都默認(rèn)是逐元素進(jìn)行的;這就叫“廣播”。
從前面的例子來(lái)看,進(jìn)行逐元素乘法的 a、b 兩個(gè)數(shù)組,既可以是大小相同的多維數(shù)組,也可以其中一個(gè)是標(biāo)量、另一個(gè)是數(shù)組,甚至可以是兩個(gè)大小不同的數(shù)組,為了使得“廣播”的語(yǔ)意明確、結(jié)果清晰,其中較小的那個(gè)數(shù)組就可以被擴(kuò)展為較大數(shù)組的大小。
本小節(jié)代碼來(lái)自《NumPy 快速入門》
創(chuàng)建數(shù)組(array)的方式有不少,其中最自然的一種方式就是通過列表來(lái)生成數(shù)組。
要使用 NumPy 模塊,首先我們要在當(dāng)前的 Python 環(huán)境中導(dǎo)入 NumPy,同時(shí)為了便于之后的引用,我們將其重命名為np
:
>>> import numpy as np
文章前面我們提到過,NumPy 的多維數(shù)組和 Python 內(nèi)置的列表長(zhǎng)得很像,估計(jì)是表親還是啥的。并且嵌套的列表在一定程度上也能夠?qū)崿F(xiàn)多維數(shù)組的功能,因此 NumPy 也很人性化的提供了接口,可以將現(xiàn)成的列表轉(zhuǎn)換為我們要的多維數(shù)組。
>>> first_array = np.array([2,3,4])
>>> first_array
array([2, 3, 4])
>>> first_array.dtype
dtype('int32')
其中,多維數(shù)組的dtype
屬性指明了多維數(shù)組中元素的類型。當(dāng)然也可以將列表變量作為參數(shù):
>>> example_list = [2.0,3.0,4.0]
>>> second_array = np.array(example_list)
>>> second_array
array([2., 3., 4.])
>>> second_array.dtype
dtype('float64')
要注意的是,通過這種方式創(chuàng)建數(shù)組,經(jīng)常犯的一個(gè)錯(cuò)誤是缺少了列表的方括號(hào),這樣參數(shù)就不再是一個(gè)列表,而是好幾個(gè)獨(dú)立的參數(shù)了:
>>> a = np.array(1,2,3,4) # 這是錯(cuò)的
>>> a = np.array([1,2,3,4]) # 這樣才是對(duì)的
實(shí)際上不僅是列表,同為 Python 中的序列類型,列表的親兄弟元組也可以起到相同的作用:
>>> a = np.array((2,3,4))
>>> a
array([2, 3, 4])
同時(shí),使用array
創(chuàng)建數(shù)組時(shí),如果提供的序列對(duì)象是嵌套的,NumPy 還可以直接據(jù)此生成二維、三維甚至更高維的多維數(shù)組:
>>> a = np.array(((2,3,4,5),(4,5,6,7)))
>>> a
array([[2, 3, 4, 5],
[4, 5, 6, 7]])
>>>
>>> a = np.array([(2,3,4,5),(4,5,6,7)])
>>> a
array([[2, 3, 4, 5],
[4, 5, 6, 7]])
還能在創(chuàng)建數(shù)組的同時(shí)顯式指定數(shù)據(jù)類型:
>>> complex_array = np.array([[1,2],[3,4]], dtype='complex')
>>> complex_array
array([[1.+0.j, 2.+0.j],
[3.+0.j, 4.+0.j]])
>>>
>>> float_array = np.array([[1,2],[3,4]], dtype='float64')
>>> float_array
array([[1., 2.],
[3., 4.]])
一般來(lái)說(shuō),很多時(shí)候我們都是知道多維數(shù)組的大小,但不知道其元素具體的值,因此 NumPy 提供了一些函數(shù),可以創(chuàng)建以占位符初始化的固定大小的多維數(shù)組。其中,zeros
創(chuàng)建的是全為 0 的多維數(shù)組,ones
創(chuàng)建的時(shí)候全為 1 的多維數(shù)組,而empty
創(chuàng)建的則是隨機(jī)初始值的多維數(shù)組,數(shù)組大小由一個(gè)序列(即列表或元組,建議使用元組)參數(shù)給定。并且這幾種方式的默認(rèn)類型都是flloat64
。
>>> np.zeros((3,4))
array([[0., 0., 0., 0.],
[0., 0., 0., 0.],
[0., 0., 0., 0.]])
>>>
>>> np.ones((3,4))
array([[1., 1., 1., 1.],
[1., 1., 1., 1.],
[1., 1., 1., 1.]])
>>>
>>> np.empty((3,4)) # empty 的結(jié)果就是一塊沒有初始化的內(nèi)存。這里由于形狀相同,是直接取了上一個(gè)數(shù)組的值
array([[1., 1., 1., 1.],
[1., 1., 1., 1.],
[1., 1., 1., 1.]])
>>>
>>> np.empty((3,5)) # 增大數(shù)組的大小就可以得到預(yù)期的隨機(jī)結(jié)果了
array([[1.40546330e-311, 1.40546113e-311, 1.37961302e-306,
6.23053614e-307, 8.45593934e-307],
[7.56593017e-307, 8.01097889e-307, 1.78020169e-306,
7.56601165e-307, 1.02359984e-306],
[2.04719290e-306, 1.00132653e-307, 1.78021527e-306,
1.66889876e-307, 3.49694131e-317]])
NumPy 還提供了一個(gè)類似于 Python 內(nèi)置的range
的函數(shù)arange
,用以創(chuàng)建一個(gè)由等差序列組成的數(shù)組:
>>> np.arange(10,30,5)
array([10, 15, 20, 25])
>>>
>>> np.arange( 0, 2, 0.3 )
array([ 0. , 0.3, 0.6, 0.9, 1.2, 1.5, 1.8])
>>>
>>> np.arange(0.2,0.3,0.01)
array([0.2 , 0.21, 0.22, 0.23, 0.24, 0.25, 0.26, 0.27, 0.28, 0.29])
可以看到,arange
的參數(shù)還可以是浮點(diǎn)數(shù),與range
略有不同。其他的特性都差不多,不再贅述。
由于浮點(diǎn)精度的原因,使用arange
創(chuàng)建浮點(diǎn)數(shù)組時(shí),我們不能保證得到我們預(yù)期大小的數(shù)組,因此這個(gè)時(shí)候就建議使用linspace
這個(gè)函數(shù)。linspace
與arange
的區(qū)別就在于它們的第三個(gè)參數(shù):前者指定的是最終得到的元素個(gè)數(shù),而后者指定的則是元素間的步長(zhǎng)。
>>> np.linspace(0.2,0.3,9)
array([0.2 , 0.2125, 0.225 , 0.2375, 0.25 , 0.2625, 0.275 , 0.2875,
0.3 ])
多維數(shù)組是 NumPy 中最主要的對(duì)象。其實(shí)就是一個(gè)由同種元素組成的元素表,可以由元組進(jìn)行索引。在 NumPy 中,維度又被稱作“軸(axe)”。
注意,在繼續(xù)介紹數(shù)組的各種屬性之前,我們要區(qū)別開“數(shù)組的維度”和“數(shù)組某個(gè)軸上的維度”。
“數(shù)組的維度”指的是數(shù)組的“軸”數(shù),用維度空間的概念來(lái)理解,也就是數(shù)組能夠在多少個(gè)方向上具有坐標(biāo)。比如一維的線性空間中,數(shù)組就只能在 x 方向上具有坐標(biāo);對(duì)于二維的平面空間,數(shù)組就在 x 和 y 兩個(gè)方向上具有坐標(biāo)。
而“數(shù)組軸的維度”則是指的在某個(gè)特定的方向上,數(shù)組可以有幾個(gè)刻度,或者說(shuō)“層次”。比如一維數(shù)組[0,1,2]
就是在 x 方向上具有 3 個(gè)層次;二維數(shù)組[[0,1,2],[2,3,4]]
則是在 x 方向上具有 2 個(gè)層次,每個(gè)層次都是一個(gè)在 y 方向上的三個(gè)層次的一維數(shù)組,在 y 方向上具有 3 個(gè)層次,每個(gè)層次都是一個(gè)在 x 方向上具有兩個(gè)層次的一維數(shù)組。
下面介紹 NumPy 多維數(shù)組的基本屬性:
ndim
即“n dimension”的簡(jiǎn)寫。該屬性指示的是多維數(shù)組的維數(shù),或者說(shuō)是“軸數(shù)”。
字面意思。這個(gè)屬性指示的是多維數(shù)組整體的維度,或者說(shuō)是多維數(shù)組的“形狀”。是一個(gè)整型元組,每一個(gè)元素都對(duì)應(yīng)與相應(yīng)軸上的維數(shù)。對(duì) n 行 m 列的矩陣而言,它的
shape
就是(n,m)
。shape
的元素個(gè)數(shù)等于多維數(shù)組的軸數(shù)。
多維數(shù)組中元素的總個(gè)數(shù)。等于
shape
中各元素之積。
dtype
實(shí)際上是“data type”的簡(jiǎn)寫,意味著它指示的是多維數(shù)組中元素的數(shù)據(jù)類型。
多維數(shù)組中,每個(gè)元素的字節(jié)大小。等效于
dtype.itemsize
。
指示的是包含多維數(shù)組中元素實(shí)際內(nèi)存的緩沖區(qū)。通常用不到。
數(shù)組在打印的時(shí)候長(zhǎng)得跟嵌套列表差不多,但其排布都要遵循以下規(guī)律:
也就是說(shuō),一維數(shù)組按行打印,二維數(shù)組按矩陣形式打印,三維及更高維數(shù)組會(huì)打印成矩陣列表。
>>> print(np.zeros((2,))) # 一維數(shù)組
[0. 0.]
>>>
>>> print(np.zeros((2,3))) # 二維數(shù)組
[[0. 0. 0.]
[0. 0. 0.]]
>>>
>>> print(np.zeros((2,3,4))) # 三維數(shù)組
[[[0. 0. 0. 0.]
[0. 0. 0. 0.]
[0. 0. 0. 0.]]
[[0. 0. 0. 0.]
[0. 0. 0. 0.]
[0. 0. 0. 0.]]]
>>>
>>> print(np.zeros((2,3,4,5))) # 四維數(shù)組
[[[[0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0.]]
[[0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0.]]
[[0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0.]]]
[[[0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0.]]
[[0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0.]]
[[0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0.]]]]
另外,為了更靈活地使用多維數(shù)組,NumPy 還提供了reshape
方法,可以將多維數(shù)組重整為某個(gè)大小。
比如在圖像識(shí)別領(lǐng)域,就需要圖像作為機(jī)器學(xué)習(xí)輸入數(shù)據(jù),而實(shí)用的機(jī)器學(xué)習(xí)應(yīng)用圖像來(lái)源又是不確定的,因此圖像的像素陣列大小不一定一致。
使用reshape
要求參數(shù)乘積與被重整的數(shù)組元素個(gè)數(shù)相同:
>>> np.arange(12)
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
>>>
>>> np.arange(12).reshape(2,6)
array([[ 0, 1, 2, 3, 4, 5],
[ 6, 7, 8, 9, 10, 11]])
>>>
>>> np.arange(12).reshape(6,2)
array([[ 0, 1],
[ 2, 3],
[ 4, 5],
[ 6, 7],
[ 8, 9],
[10, 11]])
并且reshape
還允許缺省 1 個(gè)參數(shù)(用-1
占位),它會(huì)根據(jù)數(shù)組元素的總數(shù)和提供的其他參數(shù)自動(dòng)求出一個(gè)合適的值,從而得到新的大小的數(shù)組:
>>> np.arange(12).reshape(2,3,2)
array([[[ 0, 1],
[ 2, 3],
[ 4, 5]],
[[ 6, 7],
[ 8, 9],
[10, 11]]])
>>>
>>> np.arange(12).reshape(2,-1,2)
array([[[ 0, 1],
[ 2, 3],
[ 4, 5]],
[[ 6, 7],
[ 8, 9],
[10, 11]]])
本節(jié)參考自《NumPy 索引》。示例代碼多來(lái)自《NumPy 索引》。
所謂索引,在 NumPy 中指的是任何用中括號(hào)來(lái)獲取數(shù)組元素值的行為。
NumPy 中,索引的方式有很多,這既使得 NumPy 更加強(qiáng)大靈活,也帶來(lái)了難于辨析的問題。
最簡(jiǎn)單的一種索引方式就是單個(gè)索引。對(duì)于一維數(shù)組,我們可以像對(duì) Python 中的序列一樣進(jìn)行索引:
>>> x = np.arange(10)
>>> x[2]
2
>>> x[-2]
8
對(duì)于二維和更高維的數(shù)組,我們可以在同一個(gè)中括號(hào)內(nèi)直接索引:
>>> x.shape = (2,5) # 等同于 x = x.reshape(2,5)
>>> x[1,3]
8
>>> x[1,-1]
9
而不必像對(duì)嵌套序列一樣,用多個(gè)中括號(hào)分別索引:
>>> y = [[0, 1, 2, 3, 4], [5, 6, 7, 8, 9]] # 這是一個(gè)列表,不是 NumPy 多維數(shù)組!
>>> y
[[0, 1, 2, 3, 4], [5, 6, 7, 8, 9]]
>>> y[1][3]
8
當(dāng)給出的索引少于數(shù)組維數(shù)(軸數(shù))時(shí),得到的會(huì)是一個(gè)數(shù)組對(duì)象:
>>> x[0]
array([0, 1, 2, 3, 4])
而對(duì)于返回的這個(gè)數(shù)組,我們又可以繼續(xù)索引,因此對(duì)于多維數(shù)組而言,也可以使用嵌套序列的索引方式,即使用多個(gè)中括號(hào)(但這種方式比一次索引要更低效,因此不推薦):
>>> x[0][2]
2
此外,NumPy 多維數(shù)組又可以像列表一樣,進(jìn)行切片(用冒號(hào)“:”):
>>> x = np.arange(10)
>>> x
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> x[2:5]
array([2, 3, 4])
>>> x[1:7:2]
array([1, 3, 5])
數(shù)組也可以用另一個(gè)數(shù)組來(lái)索引:
>>> x = np.arange(10,1,-1)
>>> x
array([10, 9, 8, 7, 6, 5, 4, 3, 2])
>>> x[np.array([3, 3, 1, 8])]
array([7, 7, 9, 2])
也可以用多個(gè)數(shù)組來(lái)進(jìn)行索引:
>>> y = np.arange(35).reshape(5,7)
>>> y
array([[ 0, 1, 2, 3, 4, 5, 6],
[ 7, 8, 9, 10, 11, 12, 13],
[14, 15, 16, 17, 18, 19, 20],
[21, 22, 23, 24, 25, 26, 27],
[28, 29, 30, 31, 32, 33, 34]])
>>> y[np.array([0,2,4]), np.array([0,1,2])]
array([ 0, 15, 30])
>>>
>>> y[np.array([0,2,4]), 1]
array([ 1, 15, 29])
還可以用布爾數(shù)組作為掩碼,篩選數(shù)組元素:
>>> b = y>20
>>> b
array([[False, False, False, False, False, False, False],
[False, False, False, False, False, False, False],
[False, False, False, False, False, False, False],
[ True, True, True, True, True, True, True],
[ True, True, True, True, True, True, True]])
>>> y[b]
array([21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34])
對(duì)應(yīng)于布爾數(shù)組中為“真”的元素就被篩選出來(lái)了。
本小節(jié)參考自《NumPy 廣播》和《NumPy 廣播機(jī)制詳解》。圖片來(lái)自《NumPy 廣播機(jī)制詳解》。
NumPy 中,各種運(yùn)算默認(rèn)都是“逐元素”進(jìn)行的。最簡(jiǎn)單的例子就是兩個(gè)明顯大小相同的數(shù)組運(yùn)算:
>>> a = np.array([1.0, 2.0, 3.0])
>>> b = np.array([2.0, 2.0, 2.0])
>>> a * b
array([ 2., 4., 6.])
但是逐元素計(jì)算有一個(gè)問題:對(duì)于形狀不太像的數(shù)組怎么辦呢?比如下面這個(gè)數(shù)組和標(biāo)量的乘法運(yùn)算:
>>> multi_array = np.array([1.0,2.0,3.0])
>>> scalar = 2.0
>>> multi_array * scalar # array([2., 4., 6.])
按“逐元素”運(yùn)算的預(yù)期,顯然我們是期望把這個(gè)標(biāo)量與數(shù)組的每一個(gè)元素相乘,來(lái)得到一個(gè)新的數(shù)組。但是 NumPy 懂得這其中的邏輯嗎?誒?好像還真的懂。
既然我們號(hào)稱 NumPy 可以用自然的方式來(lái)表達(dá)數(shù)學(xué)公式,肯定不能把一個(gè)簡(jiǎn)單的標(biāo)量乘向量弄得太復(fù)雜,同樣是直接乘就可以了,得到的結(jié)果與之前兩個(gè)大小相同的數(shù)組直接相乘是一樣的:
>>> multi_array * scalar
array([2., 4., 6.])
在這里邊,標(biāo)量scalar
就好像被擴(kuò)展為了一個(gè)跟multi_array
大小相當(dāng)?shù)臄?shù)組一樣。
在 NumPy 的廣播機(jī)制中,有一個(gè)很重要的概念叫做“相容的形狀”。只有當(dāng)兩個(gè)數(shù)組具有“相容的形狀”時(shí),“廣播”才能起作用;否則拋出異常ValueError: operands could not be broadcast together with shapes xx yy
。
所謂“相容的形狀”,指的是參與運(yùn)算的這兩個(gè)數(shù)組各個(gè)維度要么 1)相等;要么 2)其中一個(gè)數(shù)組的對(duì)應(yīng)維度為 1(不存在的維度也是 1)。
而 NumPy 比較各個(gè)維度的順序是從后往前,一次比較,就相當(dāng)于把參與運(yùn)算的數(shù)組形狀右對(duì)齊,然后若相等就再往前看,若其中一個(gè)為 1 就將其在這個(gè)維度上擴(kuò)展到更高的維度,直到第一個(gè)維度。
下面是對(duì)于上述規(guī)則一個(gè)更清晰的表述描述:
Image (3d array): 256 x 256 x 3
Scale (1d array): 3
Result (3d array): 256 x 256 x 3
A (4d array): 8 x 1 x 6 x 1
B (3d array): 7 x 1 x 5
Result (4d array): 8 x 7 x 6 x 5
但是像這樣的兩個(gè)數(shù)組就無(wú)法通過“廣播”實(shí)現(xiàn)逐元素運(yùn)算了:
A (1d array): 3
B (1d array): 4 # 最后一個(gè)維度無(wú)法匹配
A (2d array): 2 x 1
B (3d array): 8 x 4 x 3 # 倒數(shù)第二個(gè)維度無(wú)法匹配
對(duì)于一個(gè)多維數(shù)組和一個(gè)一維數(shù)組的運(yùn)算,實(shí)例如下:
>>> a = np.array([[ 0.0, 0.0, 0.0],
... [10.0, 10.0, 10.0],
... [20.0, 20.0, 20.0],
... [30.0, 30.0, 30.0]])
>>> b = np.array([1.0, 2.0, 3.0])
>>> a.shape
(4, 3)
>>> b.shape
(3,)
>>> a + b
array([[ 1., 2., 3.],
[11., 12., 13.],
[21., 22., 23.],
[31., 32., 33.]])
廣播圖示如下:
對(duì)齊之后,維度不相容是萬(wàn)萬(wàn)不行的:
>>> a = np.array([[ 0.0, 0.0, 0.0],
... [10.0, 10.0, 10.0],
... [20.0, 20.0, 20.0],
... [30.0, 30.0, 30.0]])
>>> b = np.array([1.0, 2.0, 3.0])
>>> a.shape
(4, 3)
>>> b.shape
(4,)
>>> a + b
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: operands could not be broadcast together with shapes (4,3) (4,)
圖示如下:
當(dāng)然我們也可以通過reshape
重整b
數(shù)組的形狀,以適應(yīng)廣播的要求:
>>> b.shape
(4,)
>>> b = b.reshape(4,1)
>>> b.shape
(4, 1)
>>> b
array([[1.],
[2.],
[3.],
[4.]])
>>> a + b
array([[ 1., 1., 1.],
[12., 12., 12.],
[23., 23., 23.],
[34., 34., 34.]])
這樣就沒問題了。
看了那么多枯燥的原理,我們接下來(lái)輕松一下,看看 NumPy 還能干什么。
喜歡拍照的同學(xué)都知道,圖片是由“像素(pixel)”構(gòu)成的。所謂“像素”,英文 pixel 就是“picture element”的簡(jiǎn)寫,指的是“構(gòu)成圖像的元素”。
實(shí)際上我們可以把一張圖片縱橫切分成很多小塊,這些最基本的小方格就是構(gòu)成多姿多彩的數(shù)字圖像世界的一磚一瓦。作為二維的圖像,它們像素的排布是不是跟二維數(shù)組很像?誒~ 對(duì)了,我們可以用一個(gè)很常用的圖形庫(kù)matplotlib
來(lái)讀取圖像,得到的實(shí)際上就是一個(gè) NumPy 二維數(shù)組:
>>> import matplotlib.pyplot as plt
>>> image = plt.imread("python-logo.png")
>>> image.shape
(600, 800, 3)
取下一半圖像:
>>> image_crop = image[300:,::,::]
>>> plt.imshow(image_crop)
<matplotlib.image.AxesImage object at 0x0000019C783EA2B0>
>>> plt.show()
取右邊一半的圖像:
>>> image_crop = image[:,400:,:]
>>> plt.imshow(image_crop)
<matplotlib.image.AxesImage object at 0x0000019C78BC0BE0>
>>> plt.show()
本文對(duì)我們以后經(jīng)常會(huì)用到的 NumPy 模塊進(jìn)行了簡(jiǎn)要的介紹。雖然本文篇幅已經(jīng)很長(zhǎng),但對(duì)于 NumPy 相關(guān)知識(shí)的講解依然只是滄海一粟。
本文的目的在于給讀者提供一個(gè)粗略的印象,記不住沒關(guān)系,希望在以后的使用中讀者能夠熟練掌握 NumPy 的使用。
[1]https://numpy.org/
[2]https://numpy.org/devdocs/user/whatisnumpy.html
[3]https://numpy.org/devdocs/user/quickstart.html
[4]https://numpy.org/devdocs/user/basics.indexing.html
[5]https://numpy.org/devdocs/user/basics.broadcasting.html
[6]https://numpy.org/devdocs/user/theory.broadcasting.html
示例代碼:https://github.com/JustDoPython/python-100-day
聯(lián)系客服