技巧 | 編程只用NumPy碼一個神經網絡

BP(Back Propagation)神經網絡是1986年由Rumelhart和McCelland爲首的科學家小組提出,是一種==按偏差逆傳播算法訓練的多層前饋網絡==,是目前應用最普遍的神經網絡模型之一。BP網絡能學習和存貯大量的==輸入-輸出模式映射關係==,而無需事前揭示描述這種映射關係的數學方程。它的學習規則是使用梯度降低法,經過反向傳播來不斷調整網絡的權值和閾值,使==網絡的偏差平方和最小==。BP神經網絡模型拓撲結構包括輸入層(input)、隱層(hidden layer)和輸出層(output layer)。python

注:本文將包含大量用 Python 編寫的代碼片斷。但願讀起來不會太無聊。web

Keras、TensorFlow、PyTorch 等高級框架能夠幫助咱們快速構建複雜模型。深刻研究並理解其中的理念頗有價值。下面嘗試只使用 NumPy 構建一個全運算的神經網絡,經過解決簡單的分類問題來測試模型,並將其與 Keras 構建的神經網絡進行性能比較。面試

 

密集神經網絡架構算法

磨刀不誤砍柴工編程

在開始編程以前,須要先整理一個基本的路線圖。咱們的目標是建立一個程序,該程序能建立一個擁有特定架構(層的數量和大小以及激活函數都是肯定的)的密集鏈接神經網絡。圖 1 給出了網絡的示例。最重要的是,網絡必須可訓練且能進行預測。數組

神經網絡框圖網絡

上圖顯示了在訓練神經網絡時須要執行的操做。它還顯示了在單次迭代的不一樣階段,須要更新和讀取多少參數。構建正確的數據結構並熟練地管理其狀態是任務中最困難的部分之一。數據結構

l 層的權值矩陣 W 和偏置向量 b 的維數。架構

神經網絡層初始化app

首先初始化每一層的權值矩陣 W 和偏置向量 b。在圖 3 中。先準備一個爲係數分配適當維數的清單。上標 [l] 表示當前層的索引 (從 1 數起),值 n 表示給定層中的單位數。假設描述 NN 架構的信息將以相似 Snippet 1 的列表形式傳遞到程序中,列表的每一項是一個描述單個網絡層基本參數的字典:input_dim 是輸入層信號向量的大小,output_dim 是輸出層激活向量的大小,activation 是在內層使用的激活函數。

nn_architecture = [
    {"input_dim": 2, "output_dim": 4, "activation": "relu"},
    {"input_dim": 4, "output_dim": 6, "activation": "relu"},
    {"input_dim": 6, "output_dim": 6, "activation": "relu"},
    {"input_dim": 6, "output_dim": 4, "activation": "relu"},
    {"input_dim": 4, "output_dim": 1, "activation": "sigmoid"},
]

Snippet 1:包含描述特定神經網絡參數的列表。該列表對應圖 1 所示的 NN。

若是你對這個話題很熟悉,你可能已經在腦海中聽到一個焦慮的聲音:「嘿,嘿!這裏有問題!有些領域是沒必要要的……」是的,此次你心裏的聲音是對的。前一層輸出的向量是下一層的輸入,因此實際上只知道一個向量的大小就足夠了。但我特地使用如下符號來保持全部層之間目標的一致性,使那些剛接觸這一課題的人更容易理解代碼。

def init_layers(nn_architecture, seed = 99):
    np.random.seed(seed)
    number_of_layers = len(nn_architecture)
    params_values = {}

    for idx, layer in enumerate(nn_architecture):
        layer_idx = idx + 1
        layer_input_size = layer["input_dim"]
        layer_output_size = layer["output_dim"]

        params_values['W' + str(layer_idx)] = np.random.randn(
            layer_output_size, layer_input_size) * 0.1
        params_values['b' + str(layer_idx)] = np.random.randn(
            layer_output_size, 1) * 0.1

    return params_values

Snippet 2:初始化權值矩陣和偏置向量值的函數。

最後是這一部分最主要的任務——層參數初始化。看過 Snippet 2 上的代碼並對 NumPy 有必定經驗的人會發現,矩陣 W 和向量 b 被小的隨機數填充。這種作法並不是偶然。權值不能用相同的數字初始化,否則會出現「對稱問題」。若是全部權值同樣,無論輸入 X 是多少,隱藏層中的全部單位都相同。在某種程度上,咱們在初始階段就會陷入死循環,不管訓練模型時間多長、網絡多深都沒法逃脫。線性代數是不會被抵消的。

在第一次迭代中,使用較小的數值能夠提升算法效率。經過圖 4 所示的 sigmoid 函數圖能夠看到,對於較大數值,它幾乎是平的,這十分影響 NN 的學習速度。總之,使用小隨機數進行參數初始化是一種簡單的方法,能保證咱們的算法有足夠好的起點。準備好的參數值存儲在帶有惟一標定其父層的 python 字典中。字典在函數末尾返回,所以算法的下一步是訪問它的內容。

算法中使用的激活函數

咱們將使用的函數中,有幾個函數很是簡單但功能強大。激活函數能夠寫在一行代碼中,但卻能使神經網絡表現出自身所需的非線性性能和可表達性。「沒有它們,咱們的神經網絡就會變成由多個線性函數組合而成的線性函數。」可選激活函數不少,但在這個項目中,我決定使用這兩種——sigmoid 和 ReLU。爲了可以獲得完整循環並同時進行前向和反向傳播,咱們還須要求導。

def sigmoid(Z):
    return 1/(1+np.exp(-Z))

def relu(Z):
    return np.maximum(0,Z)

def sigmoid_backward(dA, Z):
    sig = sigmoid(Z)
    return dA * sig * (1 - sig)

def relu_backward(dA, Z):
    dZ = np.array(dA, copy = True)
    dZ[Z <= 0] = 0;
    return dZ;

Snippet 3:ReLU 和 Sigmoid 激活函數及其導數。

前向傳播

設計好的神經網絡有一個簡單的架構。信息以 X 矩陣的形式沿一個方向傳遞,穿過隱藏的單元,從而獲得預測向量 Y_hat。爲了便於閱讀,我將前向傳播分解爲兩個單獨的函數——對單個層進行前向傳播和對整個 NN 進行前向傳播。

def single_layer_forward_propagation(A_prev, W_curr, b_curr, activation="relu"):
    Z_curr = np.dot(W_curr, A_prev) + b_curr

    if activation is "relu":
        activation_func = relu
    elif activation is "sigmoid":
        activation_func = sigmoid
    else:
        raise Exception('Non-supported activation function')

    return activation_func(Z_curr), Z_curr

Snippet 4:單層前向傳播步驟

這部分代碼多是最容易理解的。給定上一層的輸入信號,咱們計算仿射變換 Z,而後應用選定的激活函數。經過使用 NumPy,咱們能夠利用向量化——一次性對整個層和整批示例執行矩陣運算。這減小了迭代次數,大大加快了計算速度。除了計算矩陣 A,咱們的函數還返回一箇中間值 Z。做用是什麼呢?答案如圖 2 所示。咱們須要在反向傳播中用到 Z。

在前向傳播中使用的單個矩陣的維數

使用預設好的一層前向函數後,就能夠輕鬆地構建整個前向傳播。這個函數稍顯複雜,它的做用不只是預測,還要管理中間值的集合。它返回 Python 字典,其中包含爲特定層計算的 A 和 Z 值。

def full_forward_propagation(X, params_values, nn_architecture):
    memory = {}
    A_curr = X

    for idx, layer in enumerate(nn_architecture):
        layer_idx = idx + 1
        A_prev = A_curr

        activ_function_curr = layer["activation"]
        W_curr = params_values["W" + str(layer_idx)]
        b_curr = params_values["b" + str(layer_idx)]
        A_curr, Z_curr = single_layer_forward_propagation(A_prev, W_curr, b_curr, activ_function_curr)

        memory["A" + str(idx)] = A_prev
        memory["Z" + str(layer_idx)] = Z_curr

    return A_curr, memory

Snippnet 5:完整前向傳播步驟

損失函數

爲了觀察進度,保證正確方向,咱們一般須要計算損失函數的值。「通常來講,損失函數用來表徵咱們與『理想』解決方案的距離。」咱們根據要解決的問題來選擇損失函數,像 Keras 這樣的框架會有多種選擇。由於我計劃測試咱們的 NN 在兩類點上的分類,因此選擇二進制交叉熵,它定義以下。爲了得到更多學習過程的信息,我決定引入一個計算準確率的函數。

Snippnet 6:損失函數和準確率計算

反向傳播

許多缺少經驗的深度學習愛好者認爲反向傳播是一種難以理解的算法。微積分和線性代數的結合經常使缺少數學基礎的人望而卻步。因此若是你沒法立刻理解,也不要擔憂。相信我,咱們都經歷過這個過程。

def single_layer_backward_propagation(dA_curr, W_curr, b_curr, Z_curr, A_prev, activation="relu"):
    m = A_prev.shape[1]

    if activation is "relu":
        backward_activation_func = relu_backward
    elif activation is "sigmoid":
        backward_activation_func = sigmoid_backward
    else:
        raise Exception('Non-supported activation function')

    dZ_curr = backward_activation_func(dA_curr, Z_curr)
    dW_curr = np.dot(dZ_curr, A_prev.T) / m
    db_curr = np.sum(dZ_curr, axis=1, keepdims=True) / m
    dA_prev = np.dot(W_curr.T, dZ_curr)

    return dA_prev, dW_curr, db_curr

Snippnet 7:單層反向傳播步驟

人們經常混淆反向傳播與梯度降低,但實際上這是兩個獨立的問題。前者的目的是有效地計算梯度,然後者是利用計算獲得的梯度進行優化。在 NN 中,咱們計算關於參數的代價函數梯度(以前討論過),可是反向傳播能夠用來計算任何函數的導數。這個算法的本質是在已知各個函數的導數後,利用微分學中的鏈式法則計算出結合成的函數的導數。對於一層網絡,這個過程可用下面的公式描述。本文主要關注的是實際實現,故省略推導過程。經過公式能夠看出,預先記住中間層的 A 矩陣和 Z 矩陣的值是十分必要的。

一層中的前向和反向傳播

就像前向傳播同樣,我決定將計算分爲兩個獨立的函數。第一個函數(Snippnet7)側重一個單獨的層,能夠歸結爲用 NumPy 重寫上面的公式。第二個表示徹底反向傳播,主要在三個字典中讀取和更新值。而後計算預測向量(前向傳播結果)的代價函數導數。這很簡單,它只是重述了下面的公式。而後從末端開始遍歷網絡層,並根據圖 6 所示的圖計算全部參數的導數。最後,函數返回 python 字典,其中就有咱們想求的梯度。

def full_backward_propagation(Y_hat, Y, memory, params_values, nn_architecture):
    grads_values = {}
    m = Y.shape[1]
    Y = Y.reshape(Y_hat.shape)

    dA_prev = - (np.divide(Y, Y_hat) - np.divide(1 - Y, 1 - Y_hat));

    for layer_idx_prev, layer in reversed(list(enumerate(nn_architecture))):
        layer_idx_curr = layer_idx_prev + 1
        activ_function_curr = layer["activation"]

        dA_curr = dA_prev

        A_prev = memory["A" + str(layer_idx_prev)]
        Z_curr = memory["Z" + str(layer_idx_curr)]
        W_curr = params_values["W" + str(layer_idx_curr)]
        b_curr = params_values["b" + str(layer_idx_curr)]

        dA_prev, dW_curr, db_curr = single_layer_backward_propagation(
            dA_curr, W_curr, b_curr, Z_curr, A_prev, activ_function_curr)

        grads_values["dW" + str(layer_idx_curr)] = dW_curr
        grads_values["db" + str(layer_idx_curr)] = db_curr

    return grads_values

Snippnet 8:全反向傳播步驟

更新參數值

該方法的目標是利用梯度優化來更新網絡參數,以使目標函數更接近最小值。爲了實現這項任務,咱們使用兩個字典做爲函數參數:params_values 存儲參數的當前值;grads_values 存儲根據參數計算出的代價函數導數。雖然該優化算法很是簡單,只需對每一層應用下面的方程便可,但它能夠做爲更高級優化器的一個良好起點,因此我決定使用它,這也多是我下一篇文章的主題。

def update(params_values, grads_values, nn_architecture, learning_rate):
    for layer_idx, layer in enumerate(nn_architecture):
        params_values["W" + str(layer_idx)] -= learning_rate * grads_values["dW" + str(layer_idx)]        
        params_values["b" + str(layer_idx)] -= learning_rate * grads_values["db" + str(layer_idx)]

    return params_values;

Snippnet 9:利用梯度降低更新參數值

組合成型

任務中最困難的部分已通過去了,咱們已經準備好了全部必要的函數,如今只需把它們按正確的順序組合便可。爲了更好地理解操做順序,須要對照圖 2 的表。該函數通過訓練和期間的權值變化返回了最優權重。只須要使用接收到的權重矩陣和一組測試數據便可運行完整的前向傳播,從而進行預測。

def train(X, Y, nn_architecture, epochs, learning_rate):
    params_values = init_layers(nn_architecture, 2)
    cost_history = []
    accuracy_history = []

    for i in range(epochs):
        Y_hat, cashe = full_forward_propagation(X, params_values, nn_architecture)
        cost = get_cost_value(Y_hat, Y)
        cost_history.append(cost)
        accuracy = get_accuracy_value(Y_hat, Y)
        accuracy_history.append(accuracy)

        grads_values = full_backward_propagation(Y_hat, Y, cashe, params_values, nn_architecture)
        params_values = update(params_values, grads_values, nn_architecture, learning_rate)

    return params_values, cost_history, accuracy_history

Snippnet 10:訓練模型

David vs Goliath

若是對Python編程、網絡爬蟲、機器學習、數據挖掘、web開發、人工智能、面試經驗交流。感興趣能夠519970686,羣內會有不按期的發放免費的資料連接,這些資料都是從各個技術網站蒐集、整理出來的,若是你有好的學習資料能夠私聊發我,我會註明出處以後分享給你們。

如今能夠檢驗咱們的模型在簡單的分類問題上的表現了。我生成了一個由兩類點組成的數據集,如圖 7 所示。而後讓模型學習對兩類點分類。爲了便於比較,我還在高級框架中編寫了 Keras 模型。兩種模型具備相同的架構和學習速率。儘管如此,這樣對比仍是稍有不公,由於咱們準備的測試太過於簡單。最終,NumPy 模型和 Keras 模型在測試集上的準確率都達到了 95%,可是咱們的模型須要多花幾十倍的時間才能達到這樣的準確率。在我看來,這種狀態主要是因爲缺少適當的優化。

測試數據集

兩種模型實現的分類邊界可視化

原文連接:https://blog.csdn.net/Stephen_shijun/article/details/83507846

相關文章
相關標籤/搜索