在前面的文章中,我們簡單介紹了在 MegEngine imperative 中的各模塊以及它們的作用。對於新用户而言可能不太瞭解各個模塊的使用方法,對於模塊的結構和原理也是一頭霧水。Python 作為現在深度學習領域的主流編程語言,其相關的模塊自然也是深度學習框架的重中之重。
模塊串講將對 MegEngine 的 python 層相關模塊分別進行更加深入的介紹,會涉及到一些原理的解釋和代碼解讀。Python 層模塊串講共分為上、中、下三個部分,本文將介紹 Python 層的 functional、module 和 optimizer 模塊。理解並掌握這幾個模塊對於高效搭建神經網絡非常重要。
Python 層計算接口 —— functional 模塊
我們在定義網絡結構時經常需要包含一些計算操作,這些計算操作就定義在 functional 中。
functional 中實現了各類計算函數,包含對很多 op 的封裝,供實現模型時調用。
functional 中有些 op 完全是由 Python 代碼實現,有些則需要調用 C++ 接口完成計算(沒錯,這裏的計算就需要 MegDNN kernel)。對於後者,需要有機制確保我們的實現能夠轉發到底層正確執行,所以你在 functional 的許多 op 實現中會看到 builtin 和 apply:
-
builtinbuiltin封裝了所有的op,我們在functional中通過builtin.SomeOp(param)的方式獲得一個算子SomeOp,param表示獲取SomeOp需要的參數。 -
apply通過
builtin獲取到op後,需要調用apply接口來調用op的底層實現進行計算。apply是在Python層調用底層op的接口,apply的第一個參數是op(通過builtin獲得),後面的參數是執行這個op需要的參數,都為Tensor。在imperative中op計算通過apply(op, inputs)的方式向下傳遞並最終調用到MegDNN中的kernel`。`
Functional 中的許多 op 都需要通過 builtin 和 apply 調用底層 MegDNN 的 op 來進行計算操作。然而在實際的計算髮生前,很多時候需要在 Python 層做一些預處理。
來看下面這個例子:
def concat(inps: Iterable[Tensor], axis: int = 0, device=None) -> Tensor:
...
if len(inps) == 1:
return inps[0]
if device is None:
device = get_device(inps)
device = as_device(device)
(result,) = apply(builtin.Concat(axis=axis, comp_node=device.to_c()), *inps)
return result
這裏 concat 方法先對輸入 tensor 數量、device 在 python 層做了一些預處理,然後才調用 builtin 和 apply 向下轉發。
而對於 diag 這個 op,無需預處理直接向下傳遞即可:
def diag(inp, k=0) -> Tensor:
...
op = builtin.Diag(k=k)
(result,) = apply(op, inp)
return result
對於實現了對應 kernel 的 op,其在 imperative 層的實現通常非常的短。
上面 concat 和 diag 的 apply 調用會進入 py_apply 函數,並通過解析 Python 中的參數,將它們轉換成 C++ 中的對應類型,然後調用 imperative::apply,進入 dispatch 層。
部分 functional 的 op 不直接調用 py_apply 而是有對應的 cpp 實現,比如 squeeze:
def squeeze(inp: Tensor, axis: Optional[Union[int, Sequence[int]]] = None) -> Tensor:
return squeeze_cpp(inp, axis)
這樣的實現往往是需要在調用 py_apply 之前做一些預處理,但使用 python 實現性能較差,所以我們將相關預處理以及 py_apply 的邏輯在 C++ 層面實現。
本文主要介紹 Python 層的方法,關於 C++ 部分的實現會在之後的文章進行更深入的介紹。
在這裏我們只需要知道,functional 中包裝了所有關於 Tensor 計算相關的接口,是所有計算的入口,實際的計算操作通常會被轉發到更底層的 C++ 實現。
用户可以參考官方文檔獲取所有 functional 中的方法介紹。
模塊結構的小型封裝版本 —— module 模塊
神經網絡模型是由對輸入數據執行操作的各種層(Layer),或者説模塊(Module)組成。
Module 用來定義網絡模型結構,用户實現算法時要用組合模塊 Module (megengine/module) 的方式搭建模型,定義神經網絡時有些結構經常在模型中反覆使用,將這樣的結構封裝為一個 Module,既可以減少重複代碼也降低了複雜模型編碼的難度。
一個 module 類主要有兩類函數:
-
__init__:構造函數,定義了模型各個層的大小。用户自定義的Module都源自基類 class Module,所以在構造函數中一定要先調用super().__init__(),設置Module的一些基本屬性。模型要使用的所有層 / 模塊都需要在構造函數中聲明。class Module(metaclass=ABCMeta): r"""Base Module class. Args: name: module's name, can be initialized by the ``kwargs`` parameter of child class. """ def __init__(self, name=None): self._modules = [] if name is not None: assert ( isinstance(name, str) and name.strip() ), "Module's name must be a non-empty string" self.name = name # runtime attributes self.training = True self.quantize_disabled = False # hooks self._forward_pre_hooks = OrderedDict() self._forward_hooks = OrderedDict() # used for profiler and automatic naming self._name = None self._short_name = None # 抽象方法,由繼承的 Module 自己實現 @abstractmethod def forward(self, inputs): pass # 其他方法 ... -
forward:定義模型結構,實現前向傳播,也就是將數據輸入模型到輸出的過程。這裏會調用Functional (megengine/functional)中的函數進行前向計算,forward表示的是模型實現的邏輯。來看一個例子:class Simple(Module): def __init__(self): super().__init__() self.a = Parameter([1.23], dtype=np.float32) def forward(self, x): x = x * self.a return x__init__表明模型中有一個參數a,它的初值是固定的,forward中實現了具體的計算邏輯,也就是對傳入的參數與a進行乘法運算。
對於一些更復雜的計算操作(如卷積、池化等)就需要藉助 functional 中提供的方法來完成。
除了 __init__ 和 forward,基類 class Module 提供了很多屬性和方法,常用的有:
def buffers(self, recursive: bool = True, **kwargs) -> Iterable[Tensor]:返回一個可迭代對象,遍歷當前模塊的所有buffers;def parameters(self, recursive: bool = True, **kwargs) -> Iterable[Parameter]:返回一個可迭代對象,遍歷當前模塊所有的parameters;def tensors(self, recursive: bool = True, **kwargs) -> Iterable[Parameter]:返回一個此module的Tensor的可迭代對象;def children(self, **kwargs) -> "Iterable[Module]":返回一個可迭代對象,該對象包括屬於當前模塊的直接屬性的子模塊;def named_buffers(self, prefix: Optional[str] = None, recursive: bool = True, **kwargs) -> Iterable[Tuple[str, Tensor]]:返回當前模塊中key與buffer的鍵值對的可迭代對象,這裏key是從該模塊至buffer的點路徑(dotted path);def named_parameters(self, prefix: Optional[str] = None, recursive: bool = True, **kwargs) -> Iterable[Tuple[str, Parameter]]:返回當前模塊中key與parameter的鍵值對的可迭代對象,這裏key是從該模塊至buffer的點路徑(dotted path);def named_tensors(self, prefix: Optional[str] = None, recursive: bool = True, **kwargs) -> Iterable[Tuple[str, Tensor]]:返回當前模塊中key與Tensor(buffer + parameter) 的鍵值對的可迭代對象,這裏key是從該模塊至Tensor的點路徑(dotted path);def named_modules(self, prefix: Optional[str] = None, **kwargs) -> "Iterable[Tuple[str, Module]]":返回一個可迭代對象,該對象包括當前模塊自身在內的其內部所有模塊組成的key-module鍵-模塊對,這裏key是從該模塊至各子模塊的點路徑(dotted path);def named_children(self, **kwargs) -> "Iterable[Tuple[str, Module]]":返回一個可迭代對象,該對象包括當前模塊的所有子模塊(submodule)與鍵(key)組成的key-submodule對,這裏key是子模塊對應的屬性名;def state_dict(self, rst=None, prefix="", keep_var=False):返回模塊的狀態字典,狀態字典是一個保存當前模塊所有可學習的Tensor(buffer + parameter)的字典。出於兼容性考慮,字典中的value的數據結構類型為numpy.ndarray(而不是Tensor),並且不可修改,是隻讀的;def load_state_dict(self, state_dict: Union[dict, Callable[[str, Tensor], Optional[np.ndarray]]], strict=True, ):加載一個模塊的狀態字典,這個方法常用於模型訓練過程的保存與加載。
值得一提的是,Parameters 和 Buffer 都是與 Module 相關的 Tensor,它們的區別可以理解為:
Parameter是模型的參數,在訓練過程中會通過反向傳播進行更新,因此值是可能改變的,常見的有weight、bias等;Buffer是模型用到的統計量,不會在反向傳播過程中更新,常見的有mean、var等。
在 MegEngine 的 module目錄 下可以看到已經有很多常見的 module 實現,用户實現自己的模型可以根據需要複用其中的模塊。
使用 optimizer 模塊優化模型參數
MegEngine 中的 optimizer 模塊實現了基於各種常見優化策略的優化器,為用户提供了包括 SGD、ADAM 在內的常見優化器實現。這些優化器能夠基於參數的梯度信息,按照算法所定義的策略執行更新。
大部分情況下用户不會自己實現優化器,這裏以 SGD 優化器為例,優化神經網絡模型參數的基本流程如下:
from megengine.autodiff import GradManager
import megengine.optimizer as optim
model = MyModel()
gm = GradManager().attach(model.parameters())
optimizer = optim.SGD(model.parameters(), lr=0.01) # lr may vary with different model
for data, label in dataset:
with gm:
pred = model(data)
loss = loss_fn(pred, label)
gm.backward()
optimizer.step().clear_grad()
- 這裏我們構造了一個優化器
optimizer,傳入參數是model需要被優化的Parameter,和learning rate; - 優化器通過執行
step()方法進行一次優化; -
優化器通過執行
clear_grad()方法清空參數梯度。-
為何要手動清空梯度?
梯度管理器執行
backward()方法時, 會將當前計算所得到的梯度以累加的形式積累到原有梯度上,而不是直接做替換。 因此對於新一輪的梯度計算,通常需要將上一輪計算得到的梯度信息清空。 何時進行梯度清空是由人為控制的,這樣可允許靈活進行梯度的累積。
-
用户也可以繼承 class Optimizer,實現自己的優化器。
以上就是關於 functional,Module,optimizer 的模塊的基本介紹,這幾個模塊是我們搭建模型訓練的最核心的部分,熟悉這部分後,我們就可以高效搭建神經網絡了。
附
更多 MegEngine 信息獲取,您可以:查看文檔和 GitHub 項目,或加入 MegEngine 用户交流 QQ 羣:1029741705。歡迎參與 MegEngine 社區貢獻,成為 Awesome MegEngineer,榮譽證書、定製禮品享不停。