深度學習入門(三):神經網絡的學習

本文爲《深度學習入門 基於Python的理論與實現》的部分讀書筆記
代碼以及圖片均參考此書

損失函數(loss function)

爲何要設定損失函數

在進行神經網絡的學習時,不能將識別精度作爲指標。因爲如果以識別精度爲指標,則參數的導數在絕大多數地方都會變爲0。假設某個神經網絡正確識別出了100 筆訓練數據中的32 筆,此時識別精度爲32%。如果以識別精度爲指標,即使稍微改變權重參數的值,識別精度也仍將保持在32%,不會出現變化。也就是說,僅僅微調參數,是無法改善識別精度的。即便識別精度有所改善,它的值也不會像32.0123 . . .%這樣連續變化,而是變爲33%、34%這樣的不連續的、離散的值。而如果把損失函數作爲指標,則當前損失函數的值可以表示爲0.92543 . . . 這樣的值。並且,如果稍微改變一下參數的值,對應的損失函數也會像0.93432 . . . 這樣發生連續性的變化。識別精度對微小的參數變化基本上沒有什麼反應,即便有反應,它的值也是不連續地、突然地變化。作爲激活函數的階躍函數也有同樣的情況。出於相同的原因,如果使用階躍函數作爲激活函數,神經網絡的學習將無法進行。如圖4-4 所示,階躍函數的導數在絕大多數地方(除了0 以外的地方)均爲0。也就是說,如果使用了階躍函數,那麼即便將損失函數作爲指標,參數的微小變化也會被階躍函數抹殺,導致損失函數的值不會產生任何變化。
在這裏插入圖片描述

均方誤差(mean squared error)

在這裏插入圖片描述

  • yk 是表示神經網絡的輸出,tk 表示監督數據(one-hot表示),k 表示數據的維數

交叉熵誤差(cross entropy error)

在這裏插入圖片描述

  • tk中只有正確解標籤的索引爲1,其他均爲0(one-hot 表示)。因此,上式實際上只計算對應正確解標籤的輸出的自然對數,即 E = -logyi ( ti == 1 ) 。該損失函數值儘量小時,yi在(0, 1)的範圍內儘量大,即輸出正確標籤的概率儘量大

mini-batch學習

使用訓練數據進行學習,嚴格來說,就是針對訓練數據計算損失函數的值,找出使該值儘可能小的參數。因此,計算損失函數時必須將所有的訓練數據作爲對象。也就是說,如果訓練數據有100個的話,我們就要把這100個損失函數的總和作爲學習的指標。如果要求所有訓練數據的損失函數的總和,以交叉熵誤差爲例,可以寫成下面的式子:
在這裏插入圖片描述

  • 通過這樣的平均化,可以獲得和訓練數據的數量無關的統一指標。比如,即便訓練數據有1000個或10000個,也可以求得單個數據的平均損失函數。
  • 然而,如果遇到大數據,數據量會有幾百萬、幾千萬之多,這種情況下以全部數據爲對象計算損失函數是不現實的。因此,我們從全部數據中選出一部分,作爲全部數據的“近似”。神經網絡的學習也是從訓練數據中選出一批數據(稱爲mini-batch, 小批量),然後對每個mini-batch 進行學習。比如,從60000 個訓練數據中隨機選擇100筆,再用這100筆數據進行學習。這種利用一小部分樣本數據來近似地計算整體的學習方式稱爲mini-batch學習
  • 計算所有訓練數據的損失進行梯度下降稱爲batch gradient descent,取全部訓練集中的一部分計算損失進行梯度下降稱爲mini-batch gradient descent。在進行batch gradient descent時,所有訓練數據的損失(loss)一定是單調遞減的,而進行mini-batch gradient descent時不能保證每一次更新後所有訓練數據的損失都下降,但總體的趨勢一定是下降的。

利用np.random.choice實現從訓練數據中的隨機抽取mini-batch:

train_size = x_train.shape[0]
batch_size = 10
batch_mask = np.random.choice(train_size, batch_size)
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]

下面用代碼實現mini-batch版的損失函數:

  • 均方誤差(mean squared error)
def mean_squared_error(y, t):
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)

    batch_size = y.shape[0]

    # 轉成one-hot標籤
    if t.size != y.size:
        tmp = t
        t = np.zeros_like(y)
        t[np.arange(batch_size), tmp.astype('int64')] = 1

    return 0.5 * np.sum((y - t)**2) / batch_size
  • 交叉熵誤差(cross entropy error)
def cross_entropy_error(y, t):
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)
        
    # 監督數據是one-hot-vector的情況下,轉換爲正確解標籤的索引
    if t.size == y.size:
        t = t.argmax(axis=1)
             
    batch_size = y.shape[0]
    return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size
def cross_entropy_error(y, t):
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)
        
    # 監督數據是one-hot-vector的情況下,轉換爲正確解標籤的索引
    if t.size == y.size:
        t = t.argmax(axis=1)
             
    batch_size = y.shape[0]
    return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size

數值微分

  • 中心差分
def numerical_diff(f, x):
	h = 1e-4 # 0.0001
	return (f(x+h) - f(x-h)) / (2*h)
  • 梯度
    在這裏插入圖片描述
    像上式這樣的由全部變量的偏導數彙總而成的向量稱爲梯度(gradient)
def _numerical_gradient_1d(f, x):
    h = 1e-4 # 0.0001
    grad = np.zeros_like(x)
    
    for idx in range(x.size):
        tmp_val = x[idx]

        x[idx] = float(tmp_val) + h
        fxh1 = f(x) # f(x+h)
        
        x[idx] = tmp_val - h 
        fxh2 = f(x) # f(x-h)
        
        grad[idx] = (fxh1 - fxh2) / (2*h)
        
        x[idx] = tmp_val # 還原值
        
    return grad

def numerical_gradient_2d(f, X):
    if X.ndim == 1:
        return _numerical_gradient_1d(f, X)
    else:
        grad = np.zeros_like(X)
        
        for idx, x in enumerate(X):
            grad[idx] = _numerical_gradient_1d(f, x)
        
        return grad

# 支持任意維數
def numerical_gradient(f, x):
    h = 1e-4 # 0.0001
    grad = np.zeros_like(x)
    
    # np.nditer: numpy迭代器對象
    it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
    while not it.finished:
        idx = it.multi_index
        tmp_val = x[idx]

        x[idx] = float(tmp_val) + h
        fxh1 = f(x) # f(x+h)
        
        x[idx] = tmp_val - h 
        fxh2 = f(x) # f(x-h)
        
        grad[idx] = (fxh1 - fxh2) / (2*h)
        
        x[idx] = tmp_val # 還原值
        it.iternext()   
        
    return grad
  • 梯度指示的方向是各點處的函數值減小最多的方向

梯度法尋找函數的最小值

  • 神經網絡必須在學習時找到最優參數(權重和偏置)。這裏所說的最優參數是指損失函數取最小值時的參數。通過巧妙地使用梯度來尋找函數最小值(或者儘可能小的值)的方法就是梯度法。這裏需要注意的是,梯度表示的是各點處的函數值減小最多的方向。因此,無法保證梯度所指的方向就是函數的最小值或者真正應該前進的方向。實際上,在複雜的函數中,梯度指示的方向基本上都不是函數值最小處。

函數的極小值、最小值以及被稱爲鞍點(saddle point)的地方,梯度爲0。極小值是局部最小值,也就是限定在某個範圍內的最小值。鞍點是從某個方向上看是極大值,從另一個方向上看則是極小值的點。雖然梯度法是要尋找梯度爲0 的地方,但是那個地方不一定就是最小值(也有可能是極小值或者鞍點)。此外,當函數很複雜且呈扁平狀時,學習可能會進入一個(幾乎)平坦的地區,陷入被稱爲“學習高原”的無法前進的停滯期。

  • 雖然梯度的方向並不一定指向最小值,但沿着它的方向能夠最大限度地減小函數的值。因此,在尋找函數的最小值(或者儘可能小的值)的位置的任務中,要以梯度的信息爲線索,決定前進的方向。

  • 在梯度法中,函數的取值從當前位置沿着梯度方向前進一定距離,然後在新的地方重新求梯度,再沿着新梯度方向前進,如此反覆,不斷地沿梯度方向前進.尋找最小值的梯度法稱爲梯度下降法(gradient descent method)

  • 用數學式來表示梯度法
    在這裏插入圖片描述
    η 表示更新量,在神經網絡的學習中,稱爲學習率(learning rate)。學習率決定在一次學習中,應該學習多少,以及在多大程度上更新參數。一般而言,這個值過大或過小,都無法抵達一個“好的位置”。過大會發散,過小學習速度慢。在神經網絡的學習中,一般會一邊改變學習率的值,一邊確認學習是否正確進行了。這種人工設定的值稱爲超參數

def numerical_gradient_descent(f, init_x, lr=0.01, step_num=100):
    x = init_x

    for i in range(step_num):
        grad = numerical_gradient(f, x)
        x -= lr * grad

    return x

下面測試一下:

if __name__ == '__main__':
    def function_2(x):
        return x[0]**2 + x[1]**2

    init_x = np.array([-3.0, 4.0])    

    lr = 0.1
    step_num = 100
    x = numerical_gradient_descent(function_2, init_x, lr=lr, step_num=step_num)

    print(x)

代碼輸出:

[-6.11110793e-10  8.14814391e-10]

可以看出非常接近f取最小值時的參數(0, 0)

神經網絡的梯度

這裏所說的梯度是指損失函數關於權重參數的梯度。
在這裏插入圖片描述
下面以單層神經網絡爲例,求出權重參數的梯度

class simpleNet:
    def __init__(self):
        self.W = np.random.randn(2,3)

    def predict(self, x):
        return np.dot(x, self.W)

    def loss(self, x, t):
        z = self.predict(x)
        y = softmax(z)
        loss = cross_entropy_error(y, t)

        return loss

x = np.array([0.6, 0.9])
t = np.array([0, 0, 1])

net = simpleNet()

f = lambda w: net.loss(x, t) # w爲僞參數,是爲了與之前定義的numerical_gradient兼容
dW = numerical_gradient(f, net.W)

print(dW)

學習算法的實現

  1. 選出mini-batch
  2. 計算梯度
  3. 更新參數
  4. 不斷重複上述步驟
  • 下面以一個二層神經網絡在mnist數據集上訓練爲例進行代碼實現

神經網絡的學習中,必須確認是否能夠正確識別訓練數據以外的其他數據,即確認是否會發生過擬合。過擬合是指,雖然訓練數據中的數字圖像能被正確辨別,但是不在訓練數據中的數字圖像卻無法被識別的現象。

下面的代碼在進行學習的過程中,會定期地對訓練數據和測試數據記錄識別精度。這裏,每經過一個
epoch,我們都會記錄下訓練數據和測試數據的識別精度。

epoch是一個單位。一個epoch表示學習中所有訓練數據均被使用過一次時的更新次數。比如,對於10000 筆訓練數據,用大小爲100筆數據的mini-batch 進行學習時,重複隨機梯度下降法100 次,所有的訓練數據就都被“看過”了A。此時,100次就是一個epoch。

代碼同時通過pickle模塊進行參數的存儲

import sys
file_path = __file__.replace('\\', '/')
dir_path = file_path[: file_path.rfind('/')] # 當前文件夾的路徑
pardir_path = dir_path[: dir_path.rfind('/')]
sys.path.append(pardir_path) # 添加上上級目錄到python模塊搜索路徑

import numpy as np
from func.gradient import numerical_gradient 
from func.activation import sigmoid, softmax, cross_entropy_error, sigmoid_grad
import matplotlib.pyplot as plt

class TwoLayerNet:
    """
    2 Fully Connected layers
    softmax with cross entropy error
    """
    def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01):
        self.params = {}
        self.params['w1'] = np.random.randn(input_size, hidden_size) * weight_init_std
        self.params['b1'] = np.zeros(hidden_size)
        self.params['w2'] = np.random.randn(hidden_size, output_size) * weight_init_std
        self.params['b2'] = np.zeros(output_size)

    def predict(self, x):
        a1 = np.dot(x, self.params['w1']) + self.params['b1']
        z1 = sigmoid(a1)
        a2 = np.dot(z1, self.params['w2']) + self.params['b2']
        y = softmax(a2)

        return y

    def loss(self, x, t):
        y = self.predict(x)
        return cross_entropy_error(y, t)

    def accuracy(self, x, t):
        y = self.predict(x)
        y = y.argmax(axis=1)
        t = t.argmax(axis=1)

        accuracy = np.sum(y == t) / x.shape[0]
        return accuracy

    def numerical_gradient(self, x, t):
        loss = lambda w: self.loss(x, t)

        grads = {}
        grads['w1'] = numerical_gradient(loss, self.params['w1'])
        grads['b1'] = numerical_gradient(loss, self.params['b1'])
        grads['w2'] = numerical_gradient(loss, self.params['w2'])
        grads['b2'] = numerical_gradient(loss, self.params['b2'])

        return grads

    def gradient(self, x, t):
        W1, W2 = self.params['w1'], self.params['w2']
        b1, b2 = self.params['b1'], self.params['b2']
        grads = {}
        
        batch_num = x.shape[0]
        
        # forward
        a1 = np.dot(x, W1) + b1
        z1 = sigmoid(a1)
        a2 = np.dot(z1, W2) + b2
        y = softmax(a2)
        
        # backward
        dy = (y - t) / batch_num
        grads['w2'] = np.dot(z1.T, dy)
        grads['b2'] = np.sum(dy, axis=0)
        
        da1 = np.dot(dy, W2.T)
        dz1 = sigmoid_grad(a1) * da1
        grads['w1'] = np.dot(x.T, dz1)
        grads['b1'] = np.sum(dz1, axis=0)

        return grads


if __name__ == '__main__':
    from dataset.mnist import load_mnist
    import pickle
    import os

    (x_train, t_train),  (x_test, t_test) = load_mnist(normalize=True, flatten=True, one_hot_label=True)
    
    # hyper parameters
    lr = 0.1
    batch_size = 100
    iters_num = 10000

    # setting
    train_flag = 1 # 進行訓練還是預測
    pretrain_flag = 0 # 加載上一次訓練的參數
    
    pkl_file_name = dir_path + '/two_layer_net.pkl'
    train_size = x_train.shape[0]
    train_loss_list = []
    train_acc_list = []
    test_acc_list = []
    best_acc = 0

    iter_per_epoch = max(int(train_size / batch_size), 1)

    net = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)

    if (pretrain_flag == 1 or train_flag == 0) and os.path.exists(pkl_file_name):
        with open(pkl_file_name, 'rb') as f:
            net.params = pickle.load(f)
            print('params loaded!')

    if train_flag == 1:
        print('start training!')
        for i in range(iters_num):
            # 選出mini-batch
            batch_mask = np.random.choice(train_size, batch_size)
            x_batch = x_train[batch_mask]
            t_batch = t_train[batch_mask]

            # 計算梯度
            # grads_numerical = net.numerical_gradient(x_batch, t_batch)
            grads = net.gradient(x_batch, t_batch)

            # 更新參數
            for key in ('w1', 'b1', 'w2', 'b2'):
                net.params[key] -= lr * grads[key]
            
            train_loss_list.append(net.loss(x_batch, t_batch))

            # 記錄學習過程
            if i % iter_per_epoch == 0:
                train_acc_list.append(net.accuracy(x_train, t_train))
                test_acc_list.append(net.accuracy(x_test, t_test))
                print("train acc, test acc | ", train_acc_list[-1], ", ", test_acc_list[-1])

                if test_acc_list[-1] > best_acc:
                    best_acc = test_acc_list[-1]
                    with open(pkl_file_name, 'wb') as f:
                        pickle.dump(net.params, f)
                        print('net params saved!')

        # 繪製圖形
        fig, axis = plt.subplots(1, 1)

        x = np.arange(len(train_acc_list))
        axis.plot(x, train_acc_list, 'r', label='train acc')
        axis.plot(x, test_acc_list, 'g--', label='test acc')
        
        markers = {'train': 'o', 'test': 's'}
        axis.set_xlabel("epochs")
        axis.set_ylabel("accuracy")
        axis.set_ylim(0, 1.0)
        axis.legend(loc='best')
        plt.show()
    else:
        print(net.accuracy(x_train[:], t_train[:]))

因爲數值微分速度實在是太慢了,所以先用誤差反向傳播來進行梯度下降(該內容在下一篇講),下面是運行一段時間後的代碼輸出,可以看到在測試集上精度不斷上升,說明代碼工作正常:

start training!
train acc, test acc |  0.09863333333333334 ,  0.0958
net params saved!
train acc, test acc |  0.78535 ,  0.7914
net params saved!
train acc, test acc |  0.8755833333333334 ,  0.8829
net params saved!
train acc, test acc |  0.8984333333333333 ,  0.902
net params saved!
train acc, test acc |  0.9081333333333333 ,  0.9125
net params saved!
train acc, test acc |  0.9149166666666667 ,  0.9181
net params saved!
train acc, test acc |  0.9202666666666667 ,  0.9222
net params saved!
train acc, test acc |  0.9244 ,  0.9271
net params saved!
train acc, test acc |  0.92815 ,  0.9273
net params saved!
train acc, test acc |  0.9319166666666666 ,  0.9323
net params saved!
train acc, test acc |  0.9342666666666667 ,  0.9351
net params saved!
train acc, test acc |  0.9372833333333334 ,  0.9365
net params saved!
train acc, test acc |  0.9393 ,  0.9386
net params saved!
train acc, test acc |  0.9423333333333334 ,  0.9404
net params saved!
train acc, test acc |  0.94355 ,  0.9424
net params saved!
train acc, test acc |  0.9461166666666667 ,  0.9437
net params saved!
train acc, test acc |  0.9478666666666666 ,  0.9455
net params saved!
  • 可以看出網絡現在沒有出現過擬合現象
    在這裏插入圖片描述
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章