歡迎訪問個人部落格網站獲取更多文章:
本文用
numpy
從零搭建了一個類似於pytorch
的深度學習框架可以用於前面文章提到的
MINST
資料集的手寫數字識別、也可以用於其他的方面Github:
以下的文字介紹在倉庫中的
README.md
檔案中有相同內容
神經網路框架使用方法及設計思想
- 在框架上基本模仿
pytorch
,用以學習神經網路的基本演算法,如前向傳播、反向傳播、各種層、各種啟用函式 - 採用面向物件的思想進行程式設計,思路較為清晰
- 想要自己手寫神經網路的同學們可以參考一下
- 程式碼大體框架較為清晰,但不否認存在醜陋的部分,以及對於
pytorch
的拙劣模仿
專案介紹
MINST_recognition
:手寫數字識別,使用
MINST
資料集訓練30輪可以達到93%準確度,訓練500輪左右達到95%準確度無法繼續上升
RNN_sin_to_cos
:使用迴圈神經網路RNN,用\(sin\)的曲線預測\(cos\)的曲線
目前仍有bug,無法正常訓練
框架介紹
與框架有關的程式碼都放在了
mtorch
資料夾中使用流程
與
pytorch
相似,需要定義自己的神經網路、損失函式、梯度下降的優化演算法等等在每一輪的訓練中,先獲取樣本輸入將其輸入到自己的神經網路中獲取輸出。然後將預測結果和期望結果交給損失函式計算
loss
,並通過loss
進行梯度的計算,最後通過優化器對神經網路的引數進行更新。結合程式碼理解更佳:
以下是使用
MINST
資料集的手寫數字識別的主體程式碼
# 定義網路 define neural network
class DigitModule(Module):
def __init__(self):
# 計算順序就會按照這裡定義的順序進行
sequential = Sequential([
layers.Linear2(in_dim=ROW_NUM * COLUM_NUM, out_dim=16, coe=2),
layers.Relu(16),
layers.Linear2(in_dim=16, out_dim=16, coe=2),
layers.Relu(16),
layers.Linear2(in_dim=16, out_dim=CLASS_NUM, coe=1),
layers.Sigmoid(CLASS_NUM)
])
super(DigitModule, self).__init__(sequential)
module = DigitModule() # 建立模型 create module
loss_func = SquareLoss(backward_func=module.backward) # 定義損失函式 define loss function
optimizer = SGD(module, lr=learning_rate) # 定義優化器 define optimizer
for i in range(EPOCH_NUM): # 共訓練EPOCH_NUM輪
trainning_loss = 0 # 計算一下當前一輪訓練的loss值,可以沒有
for data in train_loader: # 遍歷所有樣本,train_loader是可迭代物件,儲存了資料集中所有的資料
imgs, targets = data # 將資料拆分成圖片和標籤
outputs = module(imgs) # 將樣本的輸入值輸入到自己的神經網路中
loss = loss_func(outputs, targets, transform=True) # 計算loss / calculate loss
trainning_loss += loss.value
loss.backward() # 通過反向傳播計算梯度 / calculate gradiant through back propagation
optimizer.step() # 通過優化器調整模型引數 / adjust the weights of network through optimizer
if i % TEST_STEP == 0: # 每訓練TEST_STEP輪就測試一下當前訓練的成果
show_effect(i, module, loss_func, test_loader, i // TEST_STEP)
print("{} turn finished, loss of train set = {}".format(i, trainning_loss))
接下來逐個介紹編寫的類,這些類在
pytorch
中都有同名同功能的類,是仿照pytorch
來的:Module
類- 與
pytorch
不同,只能有一個Sequential
類(序列),在該類中定義好神經網路的各個層和順序,然後傳給Module
類的建構函式 - 正向傳播:呼叫
Sequential
的正向傳播 - 反向傳播:呼叫
Sequential
的反向傳播 - 目前為止,這個類的大部分功能與
Sequential
相同,只是套了個殼保證與pytorch
相同
- 與
lossfunction
- 有不同的
loss
函式,建構函式需要給他指定自己定義的神經網路的反向傳播函式 - 呼叫
loss
函式會返回一個Loss
類的物件,該類記錄了loss
值。 - 通過呼叫
Loss
類的.backward()
方法就可以實現反向傳播計算梯度 - 內部機制:
- 內部其實就是呼叫了自己定義的神經網路的反向傳播函式
- 也算是對於
pytorch
的一個拙劣模仿,完全沒必要,直接通過Module
呼叫就好
- 有不同的
優化器:
- 目前只實現了隨機梯度下降SGD
- 建構函式的引數是自己定義的
Module
。在已經計算過梯度之後,呼叫optimizer.step()
改變Module
內各個層的引數值 - 內部機制:
- 目前由於只有SGD一種演算法,所以暫時也只是一個拙劣模仿
- 就是呼叫了一下
Module.step()
,再讓Module
呼叫Sequential.step()
,最後由Sequential
呼叫內部各個層的Layer.step()
實現更新 - 梯度值在
loss.backward
的時候計算、儲存在各個層中了
Layer
類有許多不同的層
共性
- 前向傳播:
- 接受一個輸入進行前向傳播計算,輸出一個輸出
- 會將輸入儲存起來,在反向傳播中要用
- 反向傳播:
- 接受前向傳播的輸出的梯度值,計算自身引數(如Linear中的w和b)的梯度值並儲存起來
- 輸出值為前向傳播的輸入的梯度值,用來讓上一層(可能沒有)繼續進行反向傳播計算
- 這樣不同的層之間就可以進行任意的拼裝而不妨礙前向傳播、反向傳播的進行了
.step
方法- 更新自身的引數值(也可能沒有,如啟用層、池化層)
- 前向傳播:
Sequential
類這個類也是繼承自
Layer
,可以當作一層來使用它把多個層按照順序拼裝到一起,在前向、反向傳播時按照順序進行計算
結合它的
forward
、backward
方法來理解:def forward(self, x):
out = x
for layer in self.layers:
out = layer(out)
return out
def backward(self, output_gradiant):
layer_num = len(self.layers)
delta = output_gradiant
for i in range(layer_num - 1, -1, -1):
# 反向遍歷各個層, 將期望改變數反向傳播
delta = self.layers[i].backward(delta)
def step(self, lr):
for layer in self.layers:
layer.step(lr)
RNN
類:迴圈神經網路層繼承自
Layer
,由於內容比較複雜故單獨說明一下RNN
內部由一個全連線層Linear
和一個啟用層組成前向傳播
def forward(self, inputs):
"""
:param inputs: input = (h0, x) h0.shape == (batch, out_dim) x.shape == (seq, batch, in_dim)
:return: outputs: outputs.shape == (seq, batch, out_dim)
"""
h = inputs[0] # 輸入的inputs由兩部分組成
X = inputs[1]
if X.shape[2] != self.in_dim or h.shape[1] != self.out_dim:
# 檢查輸入的形狀是否有問題
raise ShapeNotMatchException(self, "forward: wrong shape: h0 = {}, X = {}".format(h.shape, X.shape))
self.seq_len = X.shape[0] # 時間序列的長度
self.inputs = X # 儲存輸入,之後的反向傳播還要用
output_list = [] # 儲存每個時間點的輸出
for x in X:
# 按時間序列遍歷input
# x.shape == (batch, in_dim), h.shape == (batch, out_dim)
h = self.activation(self.linear(np.c_[h, x]))
output_list.append(h)
self.outputs = np.stack(output_list, axis=0) # 將列表轉換成一個矩陣儲存起來
return self.outputs
反向傳播
def backward(self, output_gradiant):
"""
:param output_gradiant: shape == (seq, batch, out_dim)
:return: input_gradiant
"""
if output_gradiant.shape != self.outputs.shape:
# 期望得到(seq, batch, out_dim)形狀
raise ShapeNotMatchException(self, "__backward: expected {}, but we got "
"{}".format(self.outputs.shape, output_gradiant.shape))
input_gradients = []
# 每個time_step上的虛擬weight_gradient, 最後求平均值就是總的weight_gradient
weight_gradients = np.zeros(self.linear.weights_shape())
bias_gradients = np.zeros(self.linear.bias_shape())
batch_size = output_gradiant.shape[1]
# total_gradient: 前向傳播的時候是將x, h合成為一個矩陣,所以反向傳播也先計算這個大矩陣的梯度再拆分為x_grad, h_grad
total_gradient = np.zeros((batch_size, self.out_dim + self.in_dim))
h_gradient = None
# 反向遍歷各個時間層,計算該層的梯度值
for i in range(self.seq_len - 1, -1, -1):
# 前向傳播順序: x, h -> z -> h
# 所以反向傳播計算順序:h_grad -> z_grad -> x_grad, h_grad, w_grad, b_grad
# %%%%%%%%%%%%%%計算平均值的版本%%%%%%%%%%%%%%%%%%%%%%%
# h_gradient = (output_gradiant[i] + total_gradient[:, 0:self.out_dim]) / 2
# %%%%%%%%%%%%%%不計算平均值的版本%%%%%%%%%%%%%%%%%%%%%%%
# 計算h_grad: 這一時間點的h_grad包括輸出的grad和之前的時間點計算所得grad兩部分
h_gradient = output_gradiant[i] + total_gradient[:, 0:self.out_dim]
# w_grad和b_grad是在linear.backward()內計算的,不用手動再計算了
z_gradient = self.activation.backward(h_gradient) # 計算z_grad
total_gradient = self.linear.backward(z_gradient) # 計算x_grad和h_grad合成的大矩陣的梯度
# total_gradient 同時包含了h和x的gradient, shape == (batch, out_dim + in_dim)
x_gradient = total_gradient[:, self.out_dim:]
input_gradients.append(x_gradient)
weight_gradients += self.linear.gradients["w"]
bias_gradients += self.linear.gradients["b"]
# %%%%%%%%%%%%%%%%%%計算平均值的版本%%%%%%%%%%%%%%%%%%%%%%%
# self.linear.set_gradients(w=weight_gradients / self.seq_len, b=bias_gradients / self.seq_len)
# %%%%%%%%%%%%%%%%%%不計算平均值的版本%%%%%%%%%%%%%%%%%%%%%%%
self.linear.set_gradients(w=weight_gradients, b=bias_gradients) # 設定梯度值
list.reverse(input_gradients) # input_gradients是逆序的,最後輸出時需要reverse一下
print("sum(weight_gradients) = {}".format(np.sum(weight_gradients)))
# np.stack的作用是將列表轉變成一個矩陣
return np.stack(input_gradients), h_gradient