歡迎訪問個人部落格網站獲取更多文章:
-
本文用
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
-
-