《手寫數字識別》神經網路 學習筆記

萌新求帶發表於2020-10-26

本文主要參考《深度學習入門-基於python的理論與實現》一書。

“手寫數字識別” 可以說是機器學習界的“hello world”了,本文將簡單說下如何不使用成熟的機器學習庫,來手動實現一個神經網路。

首先,我們需要匯入訓練集和測試集:

"""load_mnist(normalize=True, flatten=True, one_hot_label=False)
	讀入MNIST資料集
    
    Parameters
    ----------
    normalize : 將影像的畫素值正規化為0.0~1.0
    one_hot_label : 
        one_hot_label為True的情況下,標籤作為one-hot陣列返回
        one-hot陣列是指[0,0,1,0,0,0,0,0,0,0]這樣的陣列
    flatten : 是否將影像展開為一維陣列
    
    Returns
    -------
    (訓練影像, 訓練標籤), (測試影像, 測試標籤)
    """

(x_train, t_train), (x_test, t_test) = load_mnist(normalize = True, one_hot_label = True)

接著,我們要實現一個雙層網路,它應該接收一系列輸入,並經過若干個隱藏層的內部處理,最終識別出數字為0-9中的哪一個。

上面提到的過程叫做前向傳播,我們通過前向傳播來得出一個結果。但是,這個結果不一定對,所以,我們同樣需要一個叫做反向傳播的東西,來修正學習模型。

在我們的例子中,會設計4個層,分別是Affine層,Relu層,Affine層,SoftmaxWithLoss層。

  • Affine層:在Affine層我們會對輸入矩陣進行形如:y = ax+b的計算,反映到矩陣上就像一次線性變換和一次平移,所以叫做仿射變換層
  • Relu層:啟用函式層,在這裡會將上一層輸出的資料進行一次運算,輸出0-1之間的一個數。至於為什麼需要啟用層而不是直接把結果傳遞下去,可以參考 https://zhuanlan.zhihu.com/p/165194685
  • SoftmaxWithLoss層:如果不訓練ANN,那麼這層是不需要的。softmax的作用就是將輸出正規化(輸出值的和為1),以便進行反向傳播的計算。

先來看下前向傳播的實現:

  1. Affine層的前向傳播:
    def forward(self, x):
        # 對應張量
        self.original_x_shape = x.shape
        x = x.reshape(x.shape[0], -1)
        self.x = x

        out = np.dot(self.x, self.W) + self.b

        return out

很簡單,就是基本就是形如y = ax + b的形式。

  1. Relu層的前向傳播:
    def forward(self, x):
        self.mask = (x <= 0)
        out = x.copy()
        out[self.mask] = 0

        return out

Relu層也很好理解,它會將傳入的x陣列大於0的元素存為False,小於0的存為True。這是為了給反向傳播使用。實際上,Relu會把大於0的元素保持不變,小於0的元素變為0。

需要注意,這裡的運算用到了numpy陣列的特性。

  1. SoftmaskWithLoss的前向傳播:
    def forward(self, x, t):
        self.t = t
        self.y = softmax(x)
        self.loss = cross_entropy_error(self.y, self.t)
        
        return self.loss

這裡的t是監督資料,通過交叉熵誤差計算損失函式,得到損失值。

接著就是前向傳播,很簡單,依次遍歷每個層的forward函式:

    def predict(self, x):
        for layer in self.layers.values():
            x = layer.forward(x)

        return x

別忘了,我們前面還提到,要想訓練ANN,需要實現反向傳播。那麼什麼叫反向傳播?

其實反向傳播就是一個對引數求導的過程,目的是使損失函式降到最低。除了反向傳播,還有一種使用數值微分求梯度的辦法來降低損失函式的值,但這種方法計算量比較大。所以我們選擇反向傳播。

先來實現以下各個層的反向傳播演算法:

  1. Affine層的反向傳播:
    def backward(self, x):
        dx = np.dot(dout, self.W.T)
        self.dW = np.dot(self.x.T, dout)
        self.db = np.sum(dout, axis=0)

		dx = dx.reshape(*self.original_x_shape)  # 還原輸入資料的形狀(對應張量)
        return dx
  1. Relu層的反向傳播:
    def backward(self, x):
        dout[self.mask] = 0
        dx = dout
        
		return dx

需要注意,這裡的運算用到了numpy陣列的特性。

  1. SoftmaskWithLoss的反向傳播:
    def backward(self, x, t):
        batch_size = self.t.shape[0]
        if self.t.size == self.y.size: # 監督資料是one-hot-vector的情況
            dx = (self.y - self.t) / batch_size
        else:
            dx = self.y.copy()
            dx[np.arange(batch_size), self.t] -= 1
            dx = dx / batch_size
        
        return dx

訓練的完整程式碼如下:

# 讀入資料
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)
# 新建一個輸入層784神經元、隱藏層50神經元、輸出層10神經元的雙層神經網路
network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)
# 更新次數
iters_num = 10000
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.1

train_loss_list = []
train_acc_list = []
test_acc_list = []
# 所有訓練資料均被使用過一次時的更新次數,假如有1w個訓練資料,batch為100,那每100次就是一個epoch
iter_per_epoch = max(train_size / batch_size, 1)

for i in range(iters_num):
	# 挑選100個索引
    batch_mask = np.random.choice(train_size, batch_size)
    # 訓練資料
    x_batch = x_train[batch_mask]
    # 結果
    t_batch = t_train[batch_mask]
    
    # 梯度
    #grad = network.numerical_gradient(x_batch, t_batch)
    grad = network.gradient(x_batch, t_batch)
    
    # 更新
    for key in ('W1', 'b1', 'W2', 'b2'):
        network.params[key] -= learning_rate * grad[key]
        
    # 記錄每次更新完梯度的損失值
    loss = network.loss(x_batch, t_batch)
    train_loss_list.append(loss)
    
    # 每訓練完一個epoch,對比下訓練資料的準確率和測試資料的準群率。避免不知不覺間發生過擬合現象
    if i % iter_per_epoch == 0:
        train_acc = network.accuracy(x_train, t_train)
        test_acc = network.accuracy(x_test, t_test)
        train_acc_list.append(train_acc)
        test_acc_list.append(test_acc)
        print(train_acc, test_acc)

相關文章