神經網路:numpy實現神經網路框架

BeityLuo 發表於 2021-08-19
框架 神經網路

歡迎訪問個人部落格網站獲取更多文章:

https://beityluo.space

  • 以下的文字介紹在倉庫中的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,可以當作一層來使用

      • 它把多個層按照順序拼裝到一起,在前向、反向傳播時按照順序進行計算

      • 結合它的forwardbackward方法來理解:

        	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