曠視MegEngine基本概念
MegEngine 是基於計算圖的深度神經網絡學習框架。 本文簡要介紹計算圖及其相關基本概念,以及它們在 MegEngine 中的實現。
計算圖(Computational Graph)
下面通過一個簡單的數學表達式 y=(w∗x)+by=(w∗x)+b 來介紹計算圖的基本概念,如下圖所示:
圖1
從中可以看到,計算圖中存在:
- 數據節點(圖中的實心圈):如輸入數據 xx 、 ww 、 bb ,運算得到的中間數據 pp ,以及最終的運算輸出 yy ;
- 計算節點(圖中的空心圈):圖中 * 和 + 分別表示計算節點 乘法 和 加法,是施加在數據節點上的運算;
- 邊(圖中的箭頭):表示數據的流向,體現了數據節點和計算節點之間的依賴關系;
如上,便是一個簡單的計算圖示例。計算圖是一個包含數據節點和計算節點的有向圖(可以是有環的,也可以是無環的), 是數學表達式的形象化表示。在深度學習領域,任何復雜的深度神經網絡本質上都可以用一個計算圖表示出來。
前向傳播
計算由計算圖表示的數學表達式的值的過程。在圖1中,變量 xx 和 ww ,從左側輸入,首先經過乘法運算得到中間結果 pp , 接着,pp 和輸入變量 bb 經過加法運算,得到右側最終的輸出 yy ,這就是一個完整的前向傳播過程。
在 MegEngine 中,用張量(Tensor)表示計算圖中的數據節點,用算子(Operator)實現數據節點之間的運算。
張量(Tensor)
與 PyTorch,TensorFlow 等深度學習框架類似,MegEngine 使用張量(Tensor)來表示計算圖中的數據。 張量(Tensor)可以看做 NumPy 中的數組,可以是標量、向量、矩陣或者多維數組。 可以通過 NumPy 或者 Python List 來創建一個 Tensor 。
import numpy as np
import megengine as mge
# 初始化一個維度為 (2, 5) 的 ndarray,並轉化成 MegEngine 的 Tensor
# 注:目前 MegEngine Tensor 不支持 float64 數值類型,所以這里顯式指定了 ndarray 的數值類型
a = mge.tensor(np.random.random((2,5)).astype('float32'))
print(a)
# 初始化一個長度為3的列表,並轉化成 Tensor
b = mge.tensor([1., 2., 3.])
print(b)
輸出:
Tensor([[0.2976 0.4078 0.5957 0.3945 0.9413]
[0.7519 0.3313 0.0913 0.3345 0.3256]], device=xpux:0)
Tensor([1. 2. 3.], device=xpux:0)
通過 dtype 屬性,可以獲取 Tensor 的數據類型;
通過 astype() 方法可以拷貝,創建一個指定數據類型的新Tensor ,原Tensor 不變。
print(a.dtype)
d = a.astype("float16")
print(d.dtype)
輸出:
<class 'numpy.float32'>
<class 'numpy.float16'>
通過 shape 屬性,可以獲取 Tensor 的形狀:
print(a.shape)
輸出為一個Tuple:
(2, 5)
通過 numpy() 方法,可以將 Tensor 轉換為 numpy.ndarray:
a = mge.tensor(np.arange(12)).reshape(2, 6).astype("float32")
print(a)
b = a.numpy()
print(b)
輸出:
Tensor([[ 0. 1. 2. 3. 4. 5.]
[ 6. 7. 8. 9. 10. 11.]], device=xpux:0)
[[ 0. 1. 2. 3. 4. 5.]
[ 6. 7. 8. 9. 10. 11.]]
通過 device 屬性,可以查詢當前 Tensor 所在的設備。創建的 Tensor 可以位於不同 device,這根據當前的環境決定。一般地,如果在創建 Tensor 時不指定 device,其 device 屬性默認為 xpux,表示當前任意一個可用的設備。如果存在 GPU 則優先使用 GPU,否則為 CPU。
print(a.device)
輸出:
xpux:0
可以在創建 Tensor 時,指定 device 為 cpu0, cpu1, …, gpu0, gpu1, … ,也可以是 cpux 或 gpux,表示當前任意一個可用的 CPU 或 GPU。
通過 to() 方法可以在另一個 device 上生成當前 Tensor 的拷貝,比如將剛剛創建的 Tensor a 遷移到 CPU 上,再遷移到 GPU 上:
# 下面代碼是否能正確執行取決於你當前所在的環境
b = a.to("cpu0")
print(b.device)
c = b.to("gpu0")
print(c.device)
輸出:
cpu0:0
gpu0:0
GPU 和 CPU 切換
MegEngine 在 GPU 和 CPU 同時存在時默認使用 GPU 進行訓練。用戶可以調用 set_default_device() 來根據自身需求設置默認計算設備。
如下代碼設置默認設備為 CPU:
import megengine as mge
# 默認使用 CPU
mge.set_default_device('cpux')
如下代碼設置默認設備為GPU:
# 默認使用 GPU
mge.set_default_device('gpux')
如果不想修改代碼,用戶也可通過環境變量 MGE_DEFAULT_DEVICE 來設置默認計算設備:
# 默認使用 CPU
export MGE_DEFAULT_DEVICE='cpux'
# 默認使用 GPU
export MGE_DEFAULT_DEVICE='gpux'
算子(Operator)
MegEngine 中通過算子 (Operator) 來表示運算。 類似於 NumPy,MegEngine 中的算子支持基於 Tensor 的常見數學運算和操作。 下面介紹幾個簡單示例:
Tensor 的加法:
a = mge.tensor([[1., 2., 2.], [5., 1., 8.]])
print(a)
b = mge.tensor([[1., 9., 1.], [1., 7., 9.]])
print(b)
print(a + b)
輸出:
Tensor([[1. 2. 2.]
[5. 1. 8.]], device=xpux:0)
Tensor([[1. 9. 1.]
[1. 7. 9.]], device=xpux:0)
Tensor([[ 2. 11. 3.]
[ 6. 8. 17.]], device=xpux:0)
Tensor 的切片:
print(a[1, :])
輸出:
Tensor([5. 1. 8.], device=xpux:0)
Tensor 形狀的更改:
a.reshape(3, 2)
輸出:
Tensor([[1. 2.]
[2. 5.]
[1. 8.]], device=xpux:0)
reshape() 的參數允許存在單個維度的缺省值,用 -1 表示。此時,reshape 會自動推理該維度的值:
# 原始維度是 (2, 3),當給出 -1 的缺省維度值時,可以推理出另一維度為 6
a = a.reshape(1, -1)
print(a.shape)
輸出:
(1, 6)
MegEngine 的 functional 提供了更多的算子,比如深度學習中常用的矩陣乘操作、卷積操作等。
Tensor 的矩陣乘:
import megengine as mge
import megengine.functional as F
a = mge.tensor(np.arange(6).reshape(2, 3)).astype('float32')
print(a)
b = mge.tensor(np.arange(6, 12).reshape(3, 2)).astype('float32')
print(b)
c = F.matmul(a, b)
print(c)
輸出:
Tensor([[0. 1. 2.]
[3. 4. 5.]], device=xpux:0)
Tensor([[ 6. 7.]
[ 8. 9.]
[10. 11.]], device=xpux:0)
Tensor([[ 28. 31.]
[100. 112.]], device=xpux:0)
更多算子可以參見 functional 部分的文檔。
求導器(Grad Manager)
神經網絡的優化,通常通過隨機梯度下降來進行。需要根據計算圖的輸出,通過鏈式求導法則,對所有的中間數據節點求梯度,這一過程被稱之為 “反向傳播”。 例如,為了得到圖1中 yy 關於輸入 ww 的梯度,反向傳播的過程如下圖所示:
圖2
首先 y=p+by=p+b ,因此 ∂y/∂p=1∂y/∂p=1 ; 接着,反向追溯,p=w∗xp=w∗x ,因此,∂p/∂w=x∂p/∂w=x 。 根據鏈式求導法則,∂y/∂w=(∂y/∂p)∗(∂p/∂w)∂y/∂w=(∂y/∂p)∗(∂p/∂w) , 因此最終 yy 關於輸入 ww 的梯度為 xx 。
MegEngine 為計算圖中的張量提供了自動求導功能,以上圖的例子說明: 假設圖中的 xx 是 shape 為 (1, 3) 的張量, ww 是 shape 為 (3, 1) 的張量, bb 是一個標量。 利用MegEngine 計算 y=x∗w+by=x∗w+b 的過程如下:
import megengine as mge
import megengine.functional as F
from megengine.autodiff import GradManager
x = mge.tensor([1., 3., 5.]).reshape(1, 3)
w = mge.tensor([2., 4., 6.]).reshape(3, 1)
b = mge.tensor(-1.)
gm = GradManager().attach([w, b]) # 新建一個求導器,綁定需要求導的變量
with gm: # 開始記錄計算圖
p = F.matmul(x, w)
y = p + b
gm.backward(y) # 計算 y 的導數
print(w.grad)
print(b.grad)
輸出:
Tensor([[1.]
[3.]
[5.]], device=xpux:0)
Tensor([1.], device=xpux:0)
可以看到,求出的梯度本身也是 Tensor。
with 代碼段中的前向運算都會被求導器記錄。可以用 record() 和 release() 來替代 with,分別控制求導器的開啟和關閉(不推薦),代碼如下所示。
gm = GradManager().attach([w, b]) # 新建一個求導器,綁定需要求導的變量
gm.record() # 開始記錄計算圖
p = F.matmul(x, w)
y = p + b
gm.backward(y) # 計算 y 的導數
gm.release() # 停止記錄計算圖並釋放資源
此外,可以使用 detach 方法,把 Tensor 當作一個常量,這樣求導器將不會對其求導。如下所示:
gm = GradManager().attach([w, b]) # 新建一個求導器,綁定需要求導的變量
with gm: # 開始記錄計算圖
p = F.matmul(x, w)
y = p + b.detach() # 停止對 b 求導
gm.backward(y) # 計算 y 的導數
print(b.grad)
輸出:
None