前言
隨着tensorflow2的發布, 構建一個復雜的深度學習模型變得簡單很多。與此同時,更多的原理及細節被隱藏起來,很容易讓人知其然而不知其所以然。如果要了解那些隱藏在簡單表象下的原理和細節,首先想到的是閱讀它的代碼。然而這並不是一件容易的事,核心原理相關代碼淹沒在大量工程優化代碼中,讓人很難抓住要點。例如tensorflow使用了GPU技術用來加速模型的訓練和預測,然而GPU技術屬於工程上的優化技術,雖然在實際應用中不可或缺(想象一下你點擊一個鏈接幾分鍾頁面才跳出來吧),但並不涉及深度學習的相關算法和原理。
本人被這個問題困擾許久之后,決定自己寫一個輕量級的深度學習框架,通過在小規模數據集上和tensorflow對比,來加深相關原理的理解。
接下來首先給出這個框架的架構設計,然后一步一步實現一個小而美的框架。
核心概念
- 模型: 表示一個執行特定任務的神經網絡,是神經網絡層的集合。
- 神經網絡層: 完成特定任務的神經元集合, 如卷積層用於捕捉數據的空間特征,循環層用於捕捉數據的序列特征。
- 損失函數: 訓練模型的算法,它的作用是在訓練時量化模型輸出和任務目標之間的差距。
- 向前傳播: 數據經過從模型的輸入層進入,模型的每一層都使用上一層的輸出作為輸入計算出一個輸出,直到最后一層(模型的輸出層)輸出結果,預測階段向前傳播過程到此結束。如果是訓練階段,最后還要把結果送入損失函數計算誤差.
- (誤差梯度)反向傳播: 在模型的訓練階段, 損失函數計算誤差之后, 即開始誤差梯度的反向傳播過程: 把梯度從模型的輸出層送入,層層傳遞,直到模型的輸入層.
- 學習率優化算法: 深度學習模型一般使用隨機梯度的方法進行訓練,這涉及到學習率的問題,學習率優化算法在模型訓練的不同階段給出合適的學習率,從而加快模型訓練速度,提升模型最終的性能。
- 正則化: 修改學習算法的算法,可以在不影響模型訓練誤差的情況下降低泛化誤差,從而提升模型性能。其本質是懲罰與任務目標相悖的行。
整體架構
主要功能
作為一個主要用來學習原理的框架,會不涉及GPU加速和分布式並行運算,這樣整個框架的架構就會變得比較簡單。框架的主要功能有:
- 定義層的接口, 使層在模型中具有統一的抽象行為.
- 定義層參數的數據結構和命名規范, 使任意層的參數可以被靈活地訪問和修改.
- 定義激活函數的接口.
- 定義模型工作做方式, 模型可以以序列的方式把層組裝在一起, 實現向前傳播和向后傳播.
- 定義損失函數的接口.
- 定義優化器接口.
- 維護訓練上下文環境,實現模型的訓練: 向前傳播, 反向傳播, 參數更新, 訓練過程記錄, 上下文環境的保存和加載.
核心類
框架主要包含以下幾個抽象類:
- Layer: 神經網絡中的層,所有層的實現都是它的子類。
- LayerParam: Layer的參數.
- Activation: 激活函數,所有激活函數實現都是它的子類。
- Model: 模型, 一個或多個層的集合。
- Optimizer: 優化器,定義了優化器的接口. 所有優化器實現都是它的子類。
- Loss: 損失函數, 定義的損失函數的接口。
- Session: 會話,集成了模型及訓練模型所需要的所有對象,用於訓練,保存,恢復模型和訓練狀態。
架構圖

上圖描述了訓練模型的過程, 樣本數據提交給Session, Session把data送入模型執行向前傳播過程, 收到模型的輸出后,把輸出數據及label送入損失函數計算誤差梯度, 然后把梯度反向送入模型執行反向傳播過程。待反向傳播過程完成,調用優化器執行更新模型的參數。上圖中GenOptimizer是廣義優化器, Optimizer是學習率專用優化器, 廣義優化器指其他所有用來更新參數的算法如: L2正則化優化算法優化.
設計約束
LayerParam
屬性:
| 名稱 | 讀/寫 | 類型 | 說明 |
|---|---|---|---|
| name | r | string | 參數名稱,必須全局唯一 |
| value | rw | darray | 參數值 |
| gradient | rw | darray | 參數值的梯度 |
| udt | rw | int | 記錄參數被跟新的次數 |
另外, 優化器可以根據需要添加屬性。
name格式: /layerName/layerName/.../paramName. 參數名使用樹形結構, layerName是參數所屬層的名字, 因此只要保證layerName全局唯一, paramName只需要保證層范圍內唯一就能滿足要求。
Layer
屬性
| 名稱 | 讀/寫 | 類型 | 說明 |
|---|---|---|---|
| layer_id | r | int | 層的全局唯一id |
| name | r | string | 層的名字, 全局唯一 |
| params | r | list | 層的參數列表 |
| inshape | r | tuple | 向前傳播時輸入數據的形狀 |
| outshape | r | tuple | 向前傳播時輸出數據的形狀 |
| activation | r | Activation | 激活函數對象 |
方法
__ init __(self, *outshape, **kargs)
outshape: 指定輸出形狀, 可以是tuple或in.
kargs: 可選參數
- activation: 激活函數的名字。
- inshape: 輸入形狀。除了第一個層外, 其他層都不用在這個方法傳入inshape。
join(self, pre_layer, *inshape=None)
把當前層和前一個層連接在一起. 使用前一個層的outshape作為當前層的inshape, 如果設置了inshape參數, 則使用inshape參數作為當前層的inshape。如果是第一個層這個方法不會被調用。
- pre_layer: 前一個層。
- inshape: 指定當前層的輸入形狀。
init_params(self)
初始化參數,由子類實現。當Layer發現inshape和outshape都已經設置了, 才會調用這個方法。
forward(self, in_batch, training=False)
向前傳播的方法。由子類實現。
- in_batch: 輸入數據.
- training: 是否處於訓練狀態.
backward(self, gradient)
反向傳播方法。由子類實現。
- gradient: 反向傳回來的梯度。
reset(self)
重置當前層的狀態。由子類實現。
Model
方法
__init __(self, layers=None)
- layers: Layer對象list.
add(self, layer)
向模型中添加一個層.
get_layer(self, index)
使用索引從model中得到一個Layer對象。Model中需要維護一個layer list, index是layer在這個list中的索引。
layer_iterator(self)
得到層的迭代器.
assemble(self)
組裝層。檢查輸入層的inshape參數是否設置。然后調用layer的join方法把layer連接在一起。
predict(self, in_batch, training=False)
向前傳播輸出模型的預測值。參數的含義和Layer的forward相同。
backward(self, gradient)
反向傳播。參數的含義和Layer的forward相同。
reset(self)
重置模型狀態。
Activation
方法
__call __(self, in_batch)
激活函數的調用方法,由子類實現。
- in_batch: 向前傳播時輸入的數據。
grad(self, gradient)
反向傳播時計算並返回激活函數的梯度。
Loss
屬性
| 名稱 | 讀/寫 | 類型 | 說明 |
|---|---|---|---|
| gradient | r | darray | 損失函數的梯度值 |
方法
__call __(self, y_true, y_pred)
損失函數調用方法。
- y_true: 數據的標簽值。
- y_pred: 由模型輸出的數據的預測值。
Optimizer
方法
__ call__(self, model)
優化器函數的調用方法.
- model: Model對象.
Session
屬性
| 名稱 | 讀/寫 | 類型 | 說明 |
|---|---|---|---|
| model | r | Model | 訓練的目標模型 |
| loss | r | Loss | 訓練模型使用的損失函數 |
| history | r | dict | 訓練歷史記錄數據 |
方法
__init __(self, model, loss, optimizer, genoptimizer=None)
- model: Model對象。
- loss: Loss對象。
- optimizer: optimizer對象。
- genoptimizer: 廣義優化器。
batch_train(self, data, label)
分批訓練。返回損失值。
- data: 輸入數據, darray。
- label: 數據標簽, darray。
save(self, fpath)
把模型和訓練上下文保存到fpath指定的位置。
load(cls, fpath)
這是一個類方法。從fpath指定位置加載模型和訓練上下文,返回已經准備就緒的Session對象。
