numpy 對數(shù)組的操作效率
NumPy數(shù)組上的計算可能非常快甜刻,也可能非常慢≌眨快速實現(xiàn)的關(guān)鍵是使用矢量化操作得院,通常通過NumPy的通用函數(shù)(ufuncs)實現(xiàn)。
慢循環(huán)
Python的默認實現(xiàn)(CPython)執(zhí)行某些操作的速度非常慢昭齐。這是由于語言的動態(tài)尿招,解釋性所致:
類型具有靈活性,因此無法像C和Fortran這樣的語言將操作序列編譯成有效的機器代碼。最近就谜,人們進行了各種嘗試來解決這一弱點:著名的例子是PyPy項目怪蔑,
它是Python的實時編譯實現(xiàn)。 Cython項目丧荐,該項目將Python代碼轉(zhuǎn)換為可編譯的C代碼缆瓣;還有Numba項目,該項目將Python代碼段轉(zhuǎn)換為快速LLVM字節(jié)碼虹统。
每種方法都有其優(yōu)點和缺點弓坞,但是可以肯定地說,這三種方法都沒有超越標準CPython引擎的范圍和普及性车荔。
- Python的相對呆板緩慢的操作渡冻,通常可以體現(xiàn)在一些重復的小操作中忧便,下面展示
In [1]: import numpy as np
In [2]: np.random.seed(0)
In [3]: def compute_rec(values):
...: output=np.empty(len(values))
...: for i in range(len(values)):
...: output[i]=1.0/values[i]
...: return output
...:
In [4]: values = np.random.randint(1, 10, size=5)
In [5]: compute_rec(values)
Out[5]: array([0.16666667, 1. , 0.25 , 0.25 , 0.125 ])
# 小數(shù)組可以看到耗時很小只有12.2 μs左右
In [6]: %timeit compute_rec(values)
12.2 μs ± 198 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
# 這里我們創(chuàng)建一個大數(shù)組來看下
In [7]: big_arr = np.random.randint(1, 100, size=1000000)
# 當隨著數(shù)組變大竟然耗時2.52 s左右族吻,這相對其他靜態(tài)語言實在太慢了
In [8]: %timeit compute_rec(big_arr)
2.52 s ± 235 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
計算這百萬個操作并存儲結(jié)果需要幾秒鐘!甚至現(xiàn)在的手機的處理速度都以Giga-FLOPS衡量時(即每秒數(shù)十億次數(shù)字運算)珠增。
不過事實證明超歌,這里的瓶頸不是操操作系統(tǒng)作本身,而是CPython在循環(huán)的每個循環(huán)中必須執(zhí)行的類型檢查和函數(shù)分派蒂教。
每次計算倒數(shù)時巍举,Python都會首先檢查對象的類型,并動態(tài)查找要用于該類型的正確函數(shù)凝垛。如果我們使用的是已編譯的代碼(靜態(tài)語言的優(yōu)勢)懊悯,則在代碼執(zhí)行之前便會知道此類型規(guī)范,并且可以更有效地計算結(jié)果梦皮。
那我們有什么辦法可以再這種情況下提高執(zhí)行效率嗎定枷? 當然,這里我們就用到了numpy的Ufuncs 操作
Ufunc
對于許多類型的操作届氢,NumPy僅為此類靜態(tài)類型的已編譯例程提供了方便的接口。這稱為向量化操作覆旭。這可以通過簡單地對數(shù)組執(zhí)行操作來實現(xiàn)退子,然后將其應用于每個元素。這種矢量化方法旨在將循環(huán)推入NumPy底層的編譯層型将,從而大大提高了執(zhí)行速度寂祥。
比較下面兩種操作:
In [9]: compute_rec(values)
Out[9]: array([0.16666667, 1. , 0.25 , 0.25 , 0.125 ])
In [10]: 1.0/values
Out[10]: array([0.16666667, 1. , 0.25 , 0.25 , 0.125 ])
···
說明可以直接除法操作可以直接作用再數(shù)組上,那我們再比較下對大數(shù)組操作的耗時時間
```py
In [15]: %timeit (1.0 / big_arr)
5.25 ms ± 129 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)
- 執(zhí)行時間幾乎降低了三個數(shù)量級
NumPy中的矢量化操作是通過ufunc實現(xiàn)的七兜,其主要目的是對NumPy數(shù)組中的值快速執(zhí)行重復的操作丸凭。 Ufunc非常靈活–在我們看到標量和數(shù)組之間的操作之前.我們也可以在兩個數(shù)組之間進行操作:
In [18]: np.arange(5) / np.arange(1,6)
# 每個對應的元素想除,要保證兩個數(shù)組size保持一致
Out[18]: array([0. , 0.5 , 0.66666667, 0.75 , 0.8 ])
而且ufunc操作不僅限于一維數(shù)組-它們還可以作用于多維數(shù)組:
In [26]: x = np.arange(9).reshape((3, 3))
...: 2 ** x
Out[26]:
array([[ 1, 2, 4],
[ 8, 16, 32],
[ 64, 128, 256]], dtype=int32)
通過ufunc使用矢量化的計算幾乎總是比使用Python循環(huán)實現(xiàn)的計算效率更高,尤其是隨著數(shù)組大小的增加惜犀。每當在Python腳本中看到這樣的循環(huán)時铛碑,都應該考慮是否可以將其替換為向量化表達式。
Ufuncs 更多應用
Ufunc有兩種形式:一元ufunc(在單個輸入上運行)和二元ufunc(在兩個輸入上運行)虽界。我們將在這里看到這兩種功能的示例汽烦。
數(shù)組算術(shù)
NumPy的ufunc使用起來非常自然,因為它們利用了Python的本機算術(shù)運算符莉御∑餐蹋可以使用標準的加,減礁叔,乘和除法:
In [27]: x = np.arange(4)
...: print("x =", x)
...: print("x + 5 =", x + 5)
...: print("x - 5 =", x - 5)
...: print("x * 2 =", x * 2)
...: print("x / 2 =", x / 2)
...: print("x // 2 =", x // 2) # floor division
x = [0 1 2 3]
x + 5 = [5 6 7 8]
x - 5 = [-5 -4 -3 -2]
x * 2 = [0 2 4 6]
x / 2 = [0. 0.5 1. 1.5]
x // 2 = [0 0 1 1]
我們甚至可以將數(shù)組當作變量參與運算
In [30]: (x+2)*3
Out[30]: array([ 6, 9, 12, 15])
這些便捷的操作符很多都時依賴相應的方法牍颈,如下
In [31]: x+2
Out[31]: array([2, 3, 4, 5])
In [32]: np.add(x,2)
Out[32]: array([2, 3, 4, 5])
下面時numpy的操作符對應的方法
+ np.add Addition (e.g., 1 + 1 = 2)
- np.subtract Subtraction (e.g., 3 - 2 = 1)
- np.negative Unary negation (e.g., -2)
* np.multiply Multiplication (e.g., 2 * 3 = 6)
/ np.divide Division (e.g., 3 / 2 = 1.5)
// np.floor_divide Floor division (e.g., 3 // 2 = 1)
** np.power Exponentiation (e.g., 2 ** 3 = 8)
% np.mod Modulus/remainder (e.g., 9 % 4 = 1)
絕對值
In [33]: x = np.array([-2, -1, 0, 1, 2])
...: abs(x)
Out[33]: array([2, 1, 0, 1, 2])
- 這里的abs就是np.absolute的別名,也可以使用np.absolute(x)或者np.abs(x)
當數(shù)組為復數(shù)時琅关,絕對值則取的時復數(shù)的模(大兄笏辍)
In [36]: np.abs(x)
Out[36]: array([2.23606798, 3.60555128, 6.70820393])
三角函數(shù)
NumPy提供了大量有用的函數(shù),三角函數(shù)是對數(shù)據(jù)科學家最有用的一些函數(shù)死姚。我們將從定義一個角度數(shù)組開始:
從0-pi 截取三個點
In [45]: theta = np.linspace(0, np.pi, 3)
In [46]: theta
Out[46]: array([0. , 1.57079633, 3.14159265])
# 計算 o pi/2 和 pi 的sin cos tan 值
In [47]: print("sin(theta) = ", np.sin(theta))
...: print("cos(theta) = ", np.cos(theta))
...: print("tan(theta) = ", np.tan(theta))
sin(theta) = [0.0000000e+00 1.0000000e+00 1.2246468e-16]
cos(theta) = [ 1.000000e+00 6.123234e-17 -1.000000e+00]
tan(theta) = [ 0.00000000e+00 1.63312394e+16 -1.22464680e-16]
同樣也可以計算反三角函數(shù) np.arcsin(x) np.arccos(x) np.arctan(x)
指數(shù)和對數(shù)
In [49]: x = [1, 2, 3]
...: print("x =", x)
...: print("e^x =", np.exp(x))
...: print("2^x =", np.exp2(x))
...: print("3^x =", np.power(3, x))
x = [1, 2, 3]
e^x = [ 2.71828183 7.3890561 20.08553692]
2^x = [2. 4. 8.]
3^x = [ 3 9 27]
## 對數(shù)函數(shù)
In [52]: x = [1, 2, 4, 10]
...: print("x =", x)
...: print("ln(x) =", np.log(x))
...: print("log2(x) =", np.log2(x))
...: print("log10(x) =", np.log10(x))
x = [1, 2, 4, 10]
ln(x) = [0. 0.69314718 1.38629436 2.30258509]
log2(x) = [0. 1. 2. 3.32192809]
log10(x) = [0. 0.30103 0.60205999 1. ]
# 當輸入的x 數(shù)值很小時人乓,下面特殊的方法可以提供更精確的結(jié)果
In [53]: x = [0, 0.001, 0.01, 0.1]
...: print("exp(x) - 1 =", np.expm1(x))
...: print("log(1 + x) =", np.log1p(x))
exp(x) - 1 = [0. 0.0010005 0.01005017 0.10517092]
log(1 + x) = [0. 0.0009995 0.00995033 0.09531018]
特別用處的ufuncs
NumPy具有更多可用的ufunc,包括雙曲三角函數(shù)都毒,按位算術(shù)等等色罚。通過查看NumPy文檔,可以發(fā)現(xiàn)很多功能账劲。
子模塊scipy.special是另一個更專業(yè)和晦澀的功能戳护。如果要在數(shù)據(jù)上計算一些晦澀的數(shù)學函數(shù),可在scipy.special中實現(xiàn)它瀑焦。有太多函數(shù)無法列出所有功能腌且,但以下代碼片段顯示了可能在統(tǒng)計上下文中出現(xiàn)的幾個功能:
##伽瑪函數(shù)(廣義階乘)和相關(guān)函數(shù)
In [56]: x = [1, 3, 4]
...: print("gamma(x) =", special.gamma(x))
...: print("ln|gamma(x)| =", special.gammaln(x))
...: print("beta(x, 2) =", special.beta(x, 2))
gamma(x) = [1. 2. 6.]
ln|gamma(x)| = [0. 0.69314718 1.79175947]
beta(x, 2) = [0.5 0.08333333 0.05 ]
#誤差函數(shù)(高斯積分)
#它的補數(shù)及其反數(shù)
In [58]: x = np.array([0, 0.3, 0.7, 1.0])
...: print("erf(x) =", special.erf(x))
...: print("erfc(x) =", special.erfc(x))
...: print("erfinv(x) =", special.erfinv(x))
erf(x) = [0. 0.32862676 0.67780119 0.84270079]
erfc(x) = [1. 0.67137324 0.32219881 0.15729921]
erfinv(x) = [0. 0.27246271 0.73286908 inf]
- NumPy和scipy.special中都有很多可用的ufunc,可以查閱官方文檔
其他Ufunc功能
指定輸出
# x數(shù)組乘以2 輸出到y(tǒng)數(shù)組,此時x數(shù)組還是原來的值
In [61]: x = np.arange(4)
...: y = np.empty(4)
...: np.multiply(x, 2, out=y)
...: print(y)
[0. 2. 4. 6.]
In [85]: x = np.arange(5)
# 輸出本身
In [86]: y = np.zeros(10)
...: np.power(2, x, out=y[::2])
Out[86]: array([ 1., 2., 4., 8., 16.])
如果我們改為寫y [:: 2] 改成 2 ** x榛瓮,這將導致創(chuàng)建一個臨時數(shù)組來保存2 ** x的結(jié)果铺董,然后執(zhí)行第二次操作,將這些值復制到y(tǒng)數(shù)組中禀晓。對于這么小的計算精续,這并沒有太大的區(qū)別,但是對于非常大的數(shù)組粹懒,謹慎使用out參數(shù)可以節(jié)省大量內(nèi)存重付。
聚合
對于二進制ufunc,可以直接從對象中計算出一些有趣的聚合凫乖。例如确垫,如果我們想通過特定操作來簡化數(shù)組弓颈,則可以使用任何ufunc的reduce方法。將給定操作删掀,重復應用于數(shù)組元素翔冀,直到僅保留單個結(jié)果為止。如下在add ufunc上調(diào)用reduce會返回數(shù)組中所有元素的總和
# 相加聚合
In [98]: x = np.arange(5)
...: np.add.reduce(x)
Out[98]: 10
# 也可以相乘聚合
In [100]: x = np.arange(1,5)
...: np.multiply.reduce(x)
Out[100]: 24
# 也可以保留執(zhí)行的中間過程
In [101]: np.add.accumulate(x)
Out[101]: array([ 1, 3, 6, 10], dtype=int32)
In [102]: np.multiply.accumulate(x)
Out[102]: array([ 1, 2, 6, 24], dtype=int32)
- 注意爬迟,對于這些特殊情況橘蜜,有專用的NumPy函數(shù)來計算結(jié)果(np.sum,np.prod付呕,np.cumsum计福,np.cumprod),可在聚合中進行探討:最小值徽职,最大值和介于兩者之間的所有值象颖。
外部的方法
任何ufunc都可以使用外部方法來計算兩個不同輸入的所有對的輸出。這樣一來姆钉,就可以執(zhí)行創(chuàng)建乘法表之類的操作:
# 相當于矩陣相乘(1说订,2,3潮瓶,4陶冷,5)*(1,2毯辅,3埂伦,4,5)
In [103]: x = np.arange(1, 6)
...: np.multiply.outer(x, x)
Out[103]:
array([[ 1, 2, 3, 4, 5],
[ 2, 4, 6, 8, 10],
[ 3, 6, 9, 12, 15],
[ 4, 8, 12, 16, 20],
[ 5, 10, 15, 20, 25]])
ufuncs的另一個極其有用的功能是能夠在不同大小和形狀的數(shù)組之間進行操作的能力思恐,這是一組稱為廣播的操作沾谜。下一節(jié)討論