NumPy is the fundamental package for scientific computing in Python.
NumPy是一個開源的Python科學計算庫。
對於相同的數值計算任務,使用NumPy比直接使用Python要簡潔、高效的多。
NumPy使用ndarray
來處理多維數組。
NumPy provides an N-dimensional array type, the ndarray, which describes a collection of “items” of the same type. The items can be indexed using for example N integers.
NumPy提供了一個N維數組類型ndarray
,它描述了相同類型的items
的集合。
比如下面的學生成績:
語文 | 數學 | 英語 | 物理 | 化學 |
---|---|---|---|---|
92 | 99 | 91 | 85 | 90 |
95 | 85 | 88 | 81 | 88 |
85 | 81 | 80 | 78 | 86 |
用ndarray
進行存儲:
import numpy as np
score = np.array(
[[92, 99, 91, 85, 90],
[95, 85, 88, 81, 88],
[85, 81, 80, 78, 86]])
score
array([[92, 99, 91, 85, 90], [95, 85, 88, 81, 88], [85, 81, 80, 78, 86]])
用數據說話。所以,這里先通過幾行代碼來比較ndarray
與Python原生的list
的執行效率。
import random
import time
import numpy as np
list = [random.random() for i in range(10000000)]
array = np.array(list)
# 使用%time魔法方法, 可查看當前行的代碼運行一次所花費的時間
%time sum_array = np.sum(array)
%time sum_list = sum(list)
CPU times: user 4.59 ms, sys: 3 µs, total: 4.59 ms Wall time: 4.6 ms CPU times: user 37.2 ms, sys: 155 µs, total: 37.4 ms Wall time: 37.2 ms
可以看到ndarray
的計算速度要快很多。機器學習通常有大量的數據運算,如果沒有一個高效的運算方案,很難流行起來。
NumPy對ndarray
的操作和運算進行了專門的設計,所以數組的存儲效率和輸入輸出性能遠優於Python中的嵌套列表,數組越大,NumPy的優勢就越明顯。
那么自然要問了,ndarray
為什么這么快?
- 在內存分配上,
ndarray
相鄰元素的地址是連續的,而python原生list
是通過二次尋址方式找到下一個元素的具體位置。如下圖所示:
其中圖片來自:
https://jakevdp.github.io/blog/2014/05/09/why-python-is-slow/
一個ndarray
占用內存中一個連續塊,並且元素的類型都是相同的。所以一旦確定了ndarray
的元素類型以及元素個數,它的內存占用就確定了。而原生list
則不同,它的每個元素在list
中其實是一個地址引用,這個地址指向存儲實際元素數據的內存空間,也就是說指向的內存不一定是連續的。
- numpy底層使用
C
語言編寫,內部解除了GIL
(全局解釋器鎖)限制。
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)
- numpy支持並行運算,系統有多個核時,條件允許的話numpy會自動發揮多核優勢。
a1 = np.array([1, 2, 3])
a1
array([1, 2, 3])
# 數組維度
a1.ndim
1
# 數組形狀
a1.shape
(3,)
# 數組元素個數
a1.size
3
# 數組元素的類型
a1.dtype
dtype('int64')
# 一個數組元素的長度(字節數)
a1.itemsize
8
ndarray
的元素類型如下表所示:
類型 | 描述 | 簡寫 |
---|---|---|
bool | 用一個字節存儲的布爾類型(True或False) | 'b' |
int8 | 一個字節大小,-128 至 127 | 'i' |
int16 | 整數,-32768 至 32767 | 'i2' |
int32 | 整數,-2^31 至 2^31 -1 | 'i4' |
int64 | 整數,-2^63 至 2^63 - 1 | 'i8' |
uint8 | 無符號整數,0 至 255 | 'u' |
uint16 | 無符號整數,0 至 65535 | 'u2' |
uint32 | 無符號整數,0 至 2^32 - 1 | 'u4' |
uint64 | 無符號整數,0 至 2^64 - 1 | 'u8' |
float16 | 半精度浮點數:16位,正負號1位,指數5位,精度10位 | 'f2' |
float32 | 單精度浮點數:32位,正負號1位,指數8位,精度23位 | 'f4' |
float64 | 雙精度浮點數:64位,正負號1位,指數11位,精度52位 | 'f8' |
complex64 | 復數,分別用兩個32位浮點數表示實部和虛部 | 'c8' |
complex128 | 復數,分別用兩個64位浮點數表示實部和虛部 | 'c16' |
object_ | python對象 | 'O' |
string_ | 字符串 | 'S' |
unicode_ | unicode類型 | 'U' |
創建數組的時候可指定元素類型。若不指定,整數默認int64
,小數默認float64
。
np.array([1, 2, 3.0]).dtype
dtype('float64')
np.array([True, True, False]).itemsize
1
np.array(['Python', 'Java', 'Golang'], dtype=np.string_).dtype
dtype('S6')
a2 = np.array([
[1, 2, 3],
[1, 2, 3]
])
a2
array([[1, 2, 3], [1, 2, 3]])
# 數組維度
a2.ndim
2
# 數組形狀
a2.shape
(2, 3)
# 數組元素個數
a2.size
6
# 數組元素類型
a2.dtype
dtype('int64')
# 一個數組元素的長度(字節數)
a2.itemsize
8
a3 = np.array([
[[1, 2, 3], [1, 2, 3]],
[[1, 2, 3], [1, 2, 3]],
[[1, 2, 3], [1, 2, 3]],
[[1, 2, 3], [1, 2, 3]]
])
a3
array([[[1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3]]])
# 數組維度
a3.ndim
3
# 數組形狀
a3.shape
(4, 2, 3)
# 數組元素個數
a3.size
24
# 數組元素類型
a3.dtype
dtype('int64')
# 一個數組元素的長度(字節數)
a3.itemsize
8
score
array([[92, 99, 91, 85, 90], [95, 85, 88, 81, 88], [85, 81, 80, 78, 86]])
# 相當於深拷貝
arr1 = np.array(score)
arr1
array([[92, 99, 91, 85, 90], [95, 85, 88, 81, 88], [85, 81, 80, 78, 86]])
# 相當於淺拷貝, 並沒有copy完整的array對象
arr2 = np.asarray(score)
arr2
array([[92, 99, 91, 85, 90], [95, 85, 88, 81, 88], [85, 81, 80, 78, 86]])
score[0, 0] = 100
score
array([[100, 99, 91, 85, 90], [ 95, 85, 88, 81, 88], [ 85, 81, 80, 78, 86]])
arr1
array([[92, 99, 91, 85, 90], [95, 85, 88, 81, 88], [85, 81, 80, 78, 86]])
arr2
array([[100, 99, 91, 85, 90], [ 95, 85, 88, 81, 88], [ 85, 81, 80, 78, 86]])
從上面的結果可以看出:傳入ndarray
時,np.array()
會copy完整的ndarray
,而np.asarray()
不會。
注意:傳入的參數是ndarray
,並非Python原生的list
。這兩種情況不能混淆。下面看下傳入list
是啥結果。
nums = [1, 2, 3]
nums
[1, 2, 3]
array1 = np.array(nums)
array1
array([1, 2, 3])
array2 = np.asarray(nums)
array2
array([1, 2, 3])
現在修改list
中的元素:
nums[0] = 10
nums
[10, 2, 3]
array1
array([1, 2, 3])
array2
array([1, 2, 3])
- 生成元素全為0的數組
np.zeros([3, 2], dtype=np.int64)
array([[0, 0], [0, 0], [0, 0]])
# Return an array of zeros with the same shape and type as a given array.
np.zeros_like(score)
array([[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]])
- 生成元素全為1的數組
np.ones([3, 2], dtype=np.int64)
array([[1, 1], [1, 1], [1, 1]])
# Return an array of ones with the same shape and type as a given array.
np.ones_like(score)
array([[1, 1, 1, 1, 1], [1, 1, 1, 1, 1], [1, 1, 1, 1, 1]])
- 創建等差數組(指定步長, 即等差數列中的公差)
# Return evenly spaced values within a given interval.
np.arange(10, 50, 5)
array([10, 15, 20, 25, 30, 35, 40, 45])
- 創建等差數組(指定元素個數)
np.linspace(2.0, 3.0, num=5)
array([2. , 2.25, 2.5 , 2.75, 3. ])
# If endpoint True, 3.0 is the last sample. Otherwise, 3.0 is not included.
np.linspace(2.0, 3.0, num=5, endpoint=False)
array([2. , 2.2, 2.4, 2.6, 2.8])
- 創建等比數列
np.logspace(2, 5, num=4, dtype=np.int64)
array([ 100, 1000, 10000, 100000])
np.logspace(2, 5, num=4, base=3, dtype=np.int64)
array([ 9, 27, 81, 243])
默認base=10.0
,第一個例子中生成num=4
個元素的等比數列,起始值是10^2
,終止值是10^5
,所以等比數列為[100, 1000, 10000, 100000]
同理,第二個例子中也是生成num=4
個元素的等比數列,不過base=3
,起始值是3^2
,終止值是3^5
,所以等比數列為[9, 27, 81, 243]
實際生產中的數據大多可能是隨機數值,而這些隨機數據往往又符合某些規律。下面會涉及到概率論的一點點知識,無需畏懼,其實初高中數學就或多或少接觸過。
# 生成均勻分布的隨機數
x1 = np.random.uniform(0, 10, 100000)
x1
array([5.76720988, 5.32880068, 7.58561359, ..., 7.59316418, 8.30197616, 4.38992042])
所謂均勻分布
,指的是相同間隔內的分布概率是等可能的。直方圖可用於較直觀地估計一個連續變量的概率分布。 下面簡要回顧一下畫直方圖的步驟,也能加深對使用場景的理解。
(1) 收集數據(數據一般應大於50個)
(2) 確定數據的極差(用數據的最大值減去最小值)
(3) 確定組距。先確定直方圖的組數,然后以此組數去除極差,可得直方圖每組的寬度,即組距
(4) 確定各組的界限值。為避免出現數據值與組界限值重合而造成頻數據計算困難,組的界限值單位應取最小測量單位的1/2
(5) 編制頻數分布表。把多個組上下界限值分別填入頻數分布表內,並把數據表中的各個數據列入相應的組,統計各組頻數據
(6) 按數據值比例畫出橫坐標
(7) 按頻數值比例畫縱坐標。以觀測值數目或百分數表示
(8) 畫直方圖。按縱坐標畫出每個長方形的高度,它代表取落在此長方形中的數據數。
下面用matplotlib
幫助我們畫圖。
import matplotlib.pyplot as plt
# 創建畫布
plt.figure(figsize=(10, 5), dpi=100)
# 畫直方圖, x代表要使用的數據,bins表示要划分區間數
plt.hist(x=x1, bins=20)
# 設置坐標軸刻度
plt.xticks(np.arange(0, 10.5, 0.5))
plt.yticks(np.arange(0, 6000, 500))
# 添加網格顯示
plt.grid(True, linestyle='--', alpha=0.8)
# 顯示圖像
plt.show()
從上圖可以直觀的看到,100000
個[0, 10)
范圍內的樣本數據,落在區間[0,0.5)
、[0.5,1.0)
、...、[9.0,9.5)
、[9.5,10.0)
內頻數都近乎5000
,符合均勻分布
規律。
正態分布也是一種概率分布。正態分布是具有兩個參數μ
和σ
的連續型隨機變量的分布,參數μ
是隨機變量的期望(即均值),決定了其位置; 參數σ
是隨機變量的標准差,決定了其分布的幅度。
若隨機變量X服從一個數學期望為μ、方差為σ^2的正態分布,記為N(μ,σ^2)。當μ = 0,σ = 1時的正態分布是標准正態分布。
類似上面的均勻分布
,我們通過生成樣本數據,畫圖觀察正態分布狀況。
已知某地區成年男性身高近似服從正態分布。下面生成均值為170,標准差為5的100000個符合正態分布規律的樣本數據。
x2 = np.random.normal(170, 5, 100000)
x2
array([177.45732513, 171.49250483, 159.53980655, ..., 156.38843943, 172.38350177, 164.87975538])
同樣使用matplotlib
幫助我們畫圖。
import matplotlib.pyplot as plt
# 創建畫布
plt.figure(figsize=(10, 5), dpi=100)
# 畫直方圖
plt.hist(x=x2, bins=100)
# 添加網格顯示
plt.grid(True, linestyle='--', alpha=0.8)
# 顯示圖像
plt.show()
從圖中我們可以看出,大多人身高都集中在170左右。講到這里,不知道你有沒有回想起高中數學講過的3σ
原則:
P(μ-σ < X ≤ μ+σ) = 68.3%
P(μ-2σ < X ≤μ+2σ) = 95.4%
P(μ-3σ < X ≤μ+3σ) = 99.7%
即:
- 數值分布在(μ-σ, μ+σ)中的概率為68.3%
- 數值分布在(μ-2σ, μ+2σ)中的概率為95.4%
- 數值分布在(μ-3σ, μ+3σ)中的概率為99.7%
可以認為,取值幾乎全部集中在(μ-3σ, μ+3σ)區間,超出這個范圍的可能性僅到0.3%
其實,生活、生產與科學實驗中很多隨機變量的概率分布都可以近似地用正態分布來描述。
數組的索引與切片類似Python中的list。下面演示一下即可。
score
array([[100, 99, 91, 85, 90], [ 95, 85, 88, 81, 88], [ 85, 81, 80, 78, 86]])
score[0]
array([100, 99, 91, 85, 90])
score[0, 1]
99
score[0, 2:4]
array([91, 85])
score[0, :-2]
array([100, 99, 91])
score[:-1, :-3]
array([[100, 99], [ 95, 85]])
score[:-1, :-3] = 100
score[:-1, :-3]
array([[100, 100], [100, 100]])
操作數據非常方便。
還記得數組的形狀是什么嗎?
score.shape
(3, 5)
(3, 5)
表示這是3行5列的二維數組。
如果現在想得到一個5行3列的二維數組呢?
# Returns an array containing the same data with a new shape
score.reshape([5, 3])
array([[100, 100, 91], [ 85, 90, 100], [100, 88, 81], [ 88, 85, 81], [ 80, 78, 86]])
score本身的形狀有變化嗎,看看此時的score啥樣?
score
array([[100, 100, 91, 85, 90], [100, 100, 88, 81, 88], [ 85, 81, 80, 78, 86]])
如果想就地修改score的形狀,應該使用resize()
:
# Change shape and size of array in-place
score.resize([5, 3])
score
array([[100, 100, 91], [ 85, 90, 100], [100, 88, 81], [ 88, 85, 81], [ 80, 78, 86]])
如果想轉置數組呢(即數組的行、列進行互換)?
score.T
array([[100, 85, 100, 88, 80], [100, 90, 88, 85, 78], [ 91, 100, 81, 81, 86]])
主意:調用數組的轉置后,score
本身並沒有改變,如下:
score
array([[100, 100, 91], [ 85, 90, 100], [100, 88, 81], [ 88, 85, 81], [ 80, 78, 86]])
# Find the unique elements of an array
# Returns the sorted unique elements of an array
np.unique(score)
array([ 78, 80, 81, 85, 86, 88, 90, 91, 100])
score.dtype
dtype('int64')
# Copy of the array, cast to a specified type.
score.astype(np.float64)
array([[100., 100., 91.], [ 85., 90., 100.], [100., 88., 81.], [ 88., 85., 81.], [ 80., 78., 86.]])
如果想操作符合某些條件的數據,應該怎么做?
# 成績是否及格(60分及以上為及格)
score >= 60
array([[ True, True, True], [ True, True, True], [ True, True, True], [ True, True, True], [ True, True, True]])
# 成績是否優秀(90分及以上為優秀)
score >= 90
array([[ True, True, True], [False, True, True], [ True, False, False], [False, False, False], [False, False, False]])
給滿足條件的數據賦值。
# 給及格的同學都加上5分
score[score >= 60] += 5
score
array([[105, 105, 96], [ 90, 95, 105], [105, 93, 86], [ 93, 90, 86], [ 85, 83, 91]])
# 分數不允許超過滿分(即100)
score[score > 100] = 100
score
array([[100, 100, 96], [ 90, 95, 100], [100, 93, 86], [ 93, 90, 86], [ 85, 83, 91]])
# 前面2個同學是否都滿分
np.all(score[:2] >= 100)
False
# 前面2個同學是否有滿分的
np.any(score[:2] >= 100)
True
# 分數大於90且小於95的置為1,否則為0
np.where(np.logical_and(score > 90, score < 95), 1, 0)
array([[0, 0, 0], [0, 0, 0], [0, 1, 0], [1, 0, 0], [0, 0, 1]])
# 分數為100或者小於90置為1,否則為0
np.where(np.logical_or(score == 100, score < 90), 1, 0)
array([[1, 1, 0], [0, 0, 1], [1, 0, 1], [0, 0, 1], [1, 1, 0]])
如果想統計分數的最大值、最小值、平均值、方差,該怎么做?
上面演示過程中,把成績都弄亂了。這里先恢復一下最開始的數據。
score = np.array(
[[92, 99, 91, 85, 90],
[95, 85, 88, 81, 88],
[85, 81, 80, 78, 86]])
score
array([[92, 99, 91, 85, 90], [95, 85, 88, 81, 88], [85, 81, 80, 78, 86]])
# 每門課的最高分(axis=0表示按照列的維度去統計)
np.max(score, axis=0)
array([95, 99, 91, 85, 90])
# 每個學生的最高分(axis=1表示按照行的維度去統計)
np.max(score, axis=1)
array([99, 95, 86])
如果想知道每門課最高分對應的是哪個同學,怎么辦?
# 每門課的最高分對應的學生(即下標)
np.argmax(score, axis=0)
array([1, 0, 0, 0, 0])
其他統計函數也都類似。
# 每門課的最低分
np.min(score, axis=0)
array([85, 81, 80, 78, 86])
# 每門課的平均分
np.mean(score, axis=0)
array([90.66666667, 88.33333333, 86.33333333, 81.33333333, 88. ])
# 每門課的中位數
np.median(score, axis=0)
array([92., 85., 88., 81., 88.])
# 每門課的方差
np.var(score, axis=0)
array([17.55555556, 59.55555556, 21.55555556, 8.22222222, 2.66666667])
# 每門課的標准差
np.std(score, axis=0)
array([4.18993503, 7.7172246 , 4.64279609, 2.86744176, 1.63299316])
arr = np.array([[1, 2, 3], [11, 22, 33]])
arr
array([[ 1, 2, 3], [11, 22, 33]])
arr * 10 + 1
array([[ 11, 21, 31], [111, 221, 331]])
通常對於兩個numpy數組的相加、相減以及相乘都是對應元素之間的操作。
arr + arr
array([[ 2, 4, 6], [22, 44, 66]])
arr - arr
array([[0, 0, 0], [0, 0, 0]])
arr / arr
array([[1., 1., 1.], [1., 1., 1.]])
arr * arr
array([[ 1, 4, 9], [ 121, 484, 1089]])
數組在進行矢量化運算時,要求數組的形狀是相等的。當兩個數組的形狀不相同的時候,可以通過擴展數組的方法來實現相加、相減、相乘等操作,這種機制叫做廣播(broadcasting
)。
大學線性代數課程中講過矩陣的知識。矩陣在這里可以看成二維數組。
矩陣乘法:(M行, N列) * (N行, L列) = (M行, L列)。計算過程如下圖所示:
下面舉一個簡單的例子說明矩陣乘法的應用。
很多學科的最終成績都是綜合平時成績與期末成績得到的,即:
平時成績 * 0.3 + 期末成績 * 0.7 = 最終成績
用矩陣乘法來計算就是:
看下在NumPy中如何計算矩陣乘法:
a = np.array([[80, 86],
[82, 80],
[85, 78],
[90, 90],
[86, 82],
[82, 90],
[78, 80],
[92, 94]])
b = np.array([[0.3], [0.7]])
np.matmul(a, b)
array([[84.2], [80.6], [80.1], [90. ], [83.2], [87.6], [79.4], [93.4]])
另外,np.dot
也可以計算矩陣乘法,如下:
np.dot(a, b)
array([[84.2], [80.6], [80.1], [90. ], [83.2], [87.6], [79.4], [93.4]])
與np.matmul
不同的是,np.dot
還可以與標量進行乘法運算:
np.dot(a, 2)
array([[160, 172], [164, 160], [170, 156], [180, 180], [172, 164], [164, 180], [156, 160], [184, 188]])