翻譯Deep Learning and the Game of Go(7)開始使用神經網絡

這一章包括

  • 介紹了人工神經網絡的基本原理
  • 講授一種識別手寫數字的網絡
  • 通過堆疊層來創建神經網絡
  • 從數據中學習從而實現一個簡單的神經網絡

本章介紹了人工神經網絡(ANN)的核心概念,它是現代深度學習的核心算法。令人驚訝的是,人工神經網絡的歷史可以追溯到20世紀40年代初。耗費了幾十年時間,它的應用程序纔在許多領域取得巨大成功,但其基本思想仍然有效。

人工神經網絡的核心是從神經科學中獲得靈感,並建立一類與我們大腦部分功能相類似的算法,特別的,我們使用神經元的概念作爲我們人工神經網絡的基本單位。神經元的羣體我們稱爲層,這些層以特定的方式相互連接來跨越網絡。給定輸入數據,神經元可以通過連接實現層層之間傳遞信息,如果信號足夠強,我們可以稱它們已經激活了。在這種方式中,數據通過網絡傳播直到最後一步,到了輸出層,我們就從中得到我們的預測。然後,這些預測可以與預期的輸出進行比較來計算預測的誤差,網絡使用該誤差來學習並改進對未來的預測。

雖然大腦啓發的架構類比有時是有用的,但我們不想在這裏過度強調它,因爲即使我們知道很多關於大腦視覺皮層方面的知識,這種類比有時還是會誤導甚至有害。我們認爲最好把人工神經網絡看作是試圖揭示生物學習的指導原則,就像造飛機使用空氣動力學而不是去複製一隻鳥。

爲了使本章更加具體,我們從零開始提供一個神經網絡的基本實現。您將應用此網絡來解決來自光學字符識別(OCR)的問題,即如何讓計算機預測手寫數字的圖像上是哪個數字。

我們的OCR數據集中的每幅圖像都是由網格上的像素組成的,您必須分析像素之間的空間關係來找出它代表的數字。圍棋,就像其他許多棋盤遊戲一樣,是在一個網格上玩的,你必須考慮棋盤上的空間關係,才能選擇一個好的落子點。你可能希望OCR的機器學習技術也能應用於圍棋這樣的遊戲。結果是,他們可以做到。第6章到第8章展示瞭如何將這些技術應用於圍棋遊戲中。

本章中,我們把相關的數學知識放得比較少。如果你不熟悉線性代數、微積分和概率論的基礎知識,或者需要一個簡短而實用的知識,我們建議你先閱讀附錄A。.此外,神經網絡學習過程中更困難的部分可以在附錄B中找到。如果你知道神經網絡,但從來沒有實現過,我們建議你直接跳轉到5.5節。如果您既熟悉又實現過神經網絡,請直接跳到第6章。在該章中,您將應用神經網絡來預測第4章中生成遊戲的落子點。 

5.1.一個簡單的用例:分類處理

在詳細介紹神經網絡之前,讓我們從一個具體的用例開始。在本節中,您將構建一個應用程序,該應用程序可以合理地預測手寫圖像數據中的數字,準確率約爲95%。值得注意的是,您只需將圖像的像素值暴露在神經網絡中,就可以做到這一點;該算法將學習提取有關數組結構本身的相關信息。

您將使用修改後的國家標準和技術研究所(MNIST)的手寫數字數據集,這是一個在機器學習和深度學習實踐中精心研究的數據集。

在本節中,您將使用NumPy庫來處理低級別數學運算。NumPy是機器學習和使用Python進行數學計算的行業標準,您將在整本書的其餘部分使用它。在嘗試本章中的任何代碼示例之前,您應該使用包管理器來安裝NumPy。如果您使用pip,請從shell中運行pip install numpy來安裝它。如果使用conda,運行conda install numpy..

5.1.1.手寫數字的MNIST數據集 

該MNIST數據集由6萬幅圖像組成,每幅圖像都是28×28像素。這些數據的幾個例子如圖5.1所示。對人類來說,識別這些圖像是一件微不足道的事情,你可以識別出第一行是7、5、3、9、3、0等。但在某些情況下,即使是人類也很難理解這幅畫所代表的內容。例如,圖5.1中的第五行的最後一個數是4或9 都有可能

在MNIST中的每個圖像都有一個標籤,一個從0到9的數字來表示圖像上描繪的真實值。

在你看數據之前,你需要先加載它。在關於這本書的GitHub倉庫的文件夾http://mng.bz/P8mn.中找到一個名爲mnist.pkl.gz的文件。

在這個文件夾中,您還將找到在本章中編寫的所有代碼。像以前一樣,我們建議您在遵循本章的流程並構建代碼庫時,也可以運行GitHub存儲庫中找到的代碼。

5.1.2.MNIST數據預處理

由於此數據集中的標籤是從0到9的整數,所以您將使用一種名爲one-hot編碼(注:又稱爲一位有效編碼,主要是採用N位狀態寄存器來對N個狀態進行編碼,每個狀態都有它獨立的寄存器位,並且在任意時候只有一位有效)的技術,將數字1先轉換爲長度爲10的全部爲0的向量,然後在下標爲1的地方放置。這種表示在機器學習中是有用的,且是廣泛使用的。爲標籤1保留向量中的一個槽,可以允許諸如神經網絡這樣的算法更容易的區分這些標籤。例如,使用one-hot編碼,數字2具有以下表示形式:[0,0,1,0,0,0,0,0,0,0]

import numpy as np
import gzip
import six.moves.cPickle as pickle


#給標籤進行one-hot編碼
def encode_label(j):
    e = np.zeros((10,1))
    e[j] = 1
    return e

one-hot編碼的好處是使得每個數字都有自己的“插槽”,這樣你就可以使用神經網絡來輸出一個輸入圖像的概率,這將在後面有用。

檢測文件mnist.pkl.gz的內容,您可以訪問三個數據池:訓練、驗證和測試數據。從第一章中回想起來,你會使用訓練數據去訓練出一個適合的機器學習算法並使用測試數據來評估算法的有效程度。驗證數據可以用來調整和驗證算法的配置,但在本章中可以被忽略。

圖片在MNIST數據集中是二維的,高度和寬度都是28個像素。您將圖像數據加載到大小爲28×28=784的特徵向量中,這裏您將完全丟棄圖像結構而只看表示爲向量的像素。這個向量的每個值表示0到1之間的灰度值,0是白色的,1是黑色的。


#加載數據的實現
def load_data_Impl():
     #加載數據文件
     file = np.load("mnist.npz")
     #得到文件裏的訓練數據
     x_train, y_train = file['x_train'], file['y_train']
     #得到文件裏的測試數據
     x_test, y_test = file['x_test'], file['y_test']
     file.close()
     return (x_train, y_train), (x_test, y_test)


#加載數據
def load_data():
   train_data,test_data = load_data_Impl()
   return shape_data(train_data),shape_data(test_data)

您現在可以簡單地表示MNIST數據集;特徵和標籤都被編碼爲向量。您的任務是設計一種機制,去學習如何準確地將特徵映射到對應的標籤上。從本質上說,您希望設計一種算法,該算法通過特徵和標籤的訓練來學習,以便它能夠預測測試特徵的標籤。

神經網絡可以很好地完成這項工作,這個你會在下一節看到。但讓我們首先討論一個天真的想法,它將向您展示您在這個應用程序必須要解決的一般問題。識別數字對人類來說是一項相對簡單的任務,但我們很難準確地解釋是如何做到這一點,以及我們如何知道我們識別的準確性,這種知道比你能解釋得還多的現象被稱爲波拉尼式悖論。這使我們特別難以明確地說明如何解決這個問題。

其中一個至關重要的方面就是模式識別——每一個手寫數字都有一定的特徵,這些特徵來源於它的原型數字版本。例如,0大致是一個橢圓形,在許多國家,一個1只是一條垂直線。考慮到這種啓發式算法,你可以通過將手寫數字與其他手寫數字進行比較來進行分類:給定一個8的圖像,這個圖像應該比任何其他數字更接近8的平均圖像。下面的average_digital函數爲您做到了這一點。

# 計算數據中所有樣本的平均值來表示一個給定的數字
def average_digit(data,digit):
    # x[0]表示特徵向量,x[1]表示標籤
    filtered_data = [x[0] for x in data if np.argmax(x[1]) == digit]
    filtered_array = np.asarray(filtered_data)
    return np.average(filtered_array, axis=0)


train, test = load_data()
# 使用平均的8來作爲簡單模型的參數來檢測8
avg_eight = average_digit(train, 8)

在你的訓練集中,這個平均8是什麼樣子的?,如下圖所示

因爲筆跡在個體之間可能有很大的不同,正如預期的那樣,平均8是有點模糊,但它形狀仍然像一個8。也許你可以使用這個在您的數據集中去識別其他的8?您使用下面的代碼來計算和顯示上圖

#顯示平均8
img = (np.reshape(avg_eight,(28,28)))
plt.imshow(img)
plt.show()

在MNIST的訓練集中,表示8的平均---avg_eight應該包含很多說明一張圖像有8的含義。您將使用avg_eight作爲參數用於判斷給定輸入向量x是否爲8的簡單模型。在神經網絡的背景下,我們提到參數時經常會提到權重,avg_eight可以表示爲你的權重

爲了方便起見,您將使用矩陣轉置方法並定義W=np.transpose(avg_eight)。然後,您可以計算W和x的點積,對W和x的值進行逐點相乘,並相加得到784個結果值。如果你的啓發式算法是正確的,如果x是一個8,個別像素應該與W相同有一個較暗的色調.。相反,如果x不是8,則應該少一點重疊。讓我們用幾個例子來驗證這個啓發式算法。

x_3 = train[2][0]
x_18 = train[17][0]
W = np.transpose(avg_eight)
#點積
np.dot(W, x_3)
np.dot(W, x_18)

你計算兩個MNIST樣本與w的點積來計算權重,一個代表4,一個代表8。你可以看到後一個結果是54.2,遠遠高,於代表4的20.1。現在,你如何決定一個結果值是否足夠高以至於能預測一個8?原則上,兩個向量的點積可以得到任何實數,你需要解決的問題是將點積的輸出轉換爲範圍0-1的數。例如,你可以定義預測8的閾值是0.5

一種方法是使用Sigmoid函數。這個Sigmoid函數通常用s來表示,來源於希臘單詞sigma。對於實數x,這個sigmoid函數定義爲:

下圖展示了這個圖像

 接下來,讓我們在Python中編寫Sigmoid函數,然後將其應用於點積的輸出。

import numpy as np


def sigmoid_double(x):
    return 1.0/(1.0+np.exp(-x))


def sigmoid(z):
    # 將函數向量化
    return np.vectorize(sigmoid_double)(z)

請注意,您有了sigmoid_double函數,它對double類型的值進行操作,還有一個在本章會廣泛使用的計算向量的sigmoid函數。在你應用sigmoid函數到你前面計算之前,請注意,Sigmoid(2)的值已經接近1,因此對於你之前計算的兩個樣本,Sigmoid(54.2)和Sigmoid(20.1)實際上是無法區分的。 你可以通過將點積的輸出向0靠近來解決這個問題,這樣做被稱爲應用了一個偏移量,我們通常用b來表示。在這個樣本中,你可以設置偏移量爲b=-45。使用權重和偏移量,你現在可以用你的模型進行計算如下

#預測使用偏移量
def predict(x, W, b):
    return sigmoid_double(np.dot(W, x) + b)
b = -45
print(predict(x_3, W, b))
print(predict(x_18, W, b))

在兩個例子x_3和x_18上得到令人滿意的結果。對後者的預測接近1,對前者的預測幾乎爲0。將輸入向量x映射到s(Wxb)的過程與x相同大小的向量W稱爲Logistic迴歸..圖5.4對長度爲4的向量進行了示意性的描述。

爲了更好地瞭解這個過程是如何工作的,讓我們計算所有訓練和測試樣本的預測結果。如前所述,您定義了一個決策閾值來決定是否可以預測爲8。作爲這裏的評估指標,您選擇精確性;您計算所有預測中正確預測的比率。 

def evaluate(data, digit, threshold, W, b): 
    total_samples = 1.0 * len(data) 
    correct_predictions = 0 
    for x in data: 
      if predict(x[0], W, b) > threshold and np.argmax(x[1]) == digit: 
         correct_predictions += 1 
      if predict(x[0], W, b) <= threshold and np.argmax(x[1]) != digit: 
         correct_predictions += 1 
    return correct_predictions / total_samples

讓我們使用這個評估函數來評估三個數據集的預測質量:訓練集、測試集和測試集中所有8的集合。你這樣做的閾值、權重和偏移量都與以前一樣。

evaluate(data=train, digit=8, threshold=0.5, W=W, b=b)  

evaluate(data=test, digit=8, threshold=0.5, W=W, b=b)   

eight_test = [x for x in test if np.argmax(x[1]) == 8]
evaluate(data=eight_test, digit=8, threshold=0.5, W=W, b=b)

你可以看到在訓練集上的準確率最高約爲78%。這不應該令人驚訝,因爲你在訓練集上校準了你的模型。值得注意的是,它並不能在訓練集上評估,因爲它無法告訴算法泛化的程度。測試數據的表現接近於訓練時的表現,大約77%。在測試集中所有是8的集合中,您只達到了66%,因此使用你的簡單模型,您只在兩個不都是8的例子中比較精確。這個結果作爲開始階段是可以接受的,但遠遠不是你能做的最好的。那到底出了什麼問題,你能做得更好嗎?

  • 您的模型能夠區分一個特定數字(這裏是一個8)與其他的數字。因爲在訓練和測試中,每個數字的圖像數量都是平衡的,只有10%左右是8。因此,一個一直預測0的模型將產生大約90%的準確性。在分析像這樣的分類問題時,經常會遇到這樣的問題。有鑑於此,你在訓練數據上的77%精確性看起來不再那麼強大了。您需要定義一個模型,它可以準確地預測所有數字
  • 您的模型的參數相當小。對於成千上萬種的手寫圖像集合,你所擁有的只是一組與其中一幅圖像大小相同的權重集合。相信你能通過使用這樣一個小的模型去捕捉到這些手寫圖像上的筆跡變化是不現實的。您必須找到一類算法,這些算法有效地使用更多的參數來捕獲數據的可變性。
  • 對於給定的預測,你只是簡單地選擇一個閾值去宣稱該數字是8還是不是8。您沒有使用實際預測值來評估模型的質量。例如,一個正確的預測在0.95,那當然表明這比精確度0.51的更加精確。你必須把預測與實際結果有多接近的概念進行形式化。
  • 你在直覺的指導下手工製作了模型的參數。即使作爲開始階段這樣做還是可以的,但是機器學習的想法是,你不必把你的觀點強加於數據之上,而是讓算法從數據中學習。當你的模型做出正確的預測時,你需要加強這種行爲,每當輸出錯誤時,您需要相應地調整您的模型。換句話說,您需要設計一種機制,根據相應的預測結果對模型進行更新。

在下一節中,您將使用直覺圍繞這個用例去處理這四個點,從而在神經網絡中邁出第一步

5.2 神經網絡的基礎

 你要如何改進你的OCR模型?正如我們在導言中所暗示的那樣,神經網絡可以在這類任務上比我們手工製作的模型要好得多。但是手工製作的模型可以說明您將用來構建神經網絡的一些關鍵概念。本節將會用神經網絡的術語去描述上一節中的模型

5.2.1 邏輯迴歸作爲簡單人工神經網絡

在5.1節中,您看到了用於二進制分類的邏輯迴歸。重述一下,您取了一個表示數據樣本的特徵向量x,將其輸入到算法中,首先將其乘以權重矩陣W,然後添加一個偏移量b。爲了輸出的預測y在0到1之間,您將應用了Sigmoid函數:y=s(Wx+b)

您應該注意到這裏的一些事情。首先,特徵向量x可以解釋爲神經元的集合,有時稱爲單元,通過W和b連接到y,您已經在圖5.4中看到了這一點。接下來,注意sigmoid函數它被看作是一個激活函數,因爲它接受Wx+b的結果並將其映射到範圍[0,1]。如果你把一個接近1的值解釋爲神經元y被激活,而反過來,接近0是沒有被激活,這個設置可以看作是人工神經網絡的一個小例子

5.2.2.多個輸出維度的網絡 

在5.1節中的用例中,您將識別手寫數字的問題簡化爲二進制分類問題;即將8與所有其他數字區分開來。但你感興趣的是要預測10個類,每個數字一個類。至少在形式上,你可以很容易地通過改變你所說的y、W和b去實現這一點;也就是你改變了模型的輸出、權重和偏差

首先,你使y成爲一個長度爲10的向量;y中的每個值,表示10的數字中每一數字的可能性:

 接下來,讓我們相應地調整權重和偏差。回想一下,目前W是長度爲784的向量。取而代之的是,您要讓W變成(10,784)的矩陣。這樣,可以讓W矩陣輸入向量x相乘,即Wx,其結果將是長度爲10的向量。緊接着,如果使偏移量變成長度爲10的向量,那麼久可以將其添加到Wx中。最後,請注意,你可以通過計算z向量的sigmoid值將應用於每個組件:

下圖描述了四個輸入和兩個輸出神經元的稍微改變的設置。 

        圖5.5 在這個簡單的網絡中,四個輸入神經元首先與一個2*4的矩陣相乘,加上一個二維偏離量,然後應用sigmoid函數,輸出一個2維向量

現在,你得到了什麼?現在可以將輸入向量x映射到輸出向量y,而以前y只是一個值。這樣做的好處是可以實現向量的多次轉換,從而構建我們所說的前饋網絡。 

5.3 前饋網絡

  讓我們快速回顧一下你在上一節中所做的事情。在一個較高的層次上,您執行了以下步驟:

  1. 您從輸入神經元x的向量開始,並應用了一個簡單的轉變,即z=Wx+b。在線性代數的概念中,這些變換被稱爲仿射線性。在這裏,您使用了z作爲中介變量
  2. 你應用了一個激活函數sigmoid:y=s(z),得到輸出神經元y。應用程序的結果告訴你被激活了多少。

前饋網絡的核心是你可以應用這個概念迭代處理,從而可以多次應用於這兩個指定的步驟去簡單構建塊。這些塊就構成了我們所說的一層。用這個概念,你可以說堆疊多層會形成多層神經網絡。讓我們通過再介紹一個層來修改我們的上一個示例。你現在必須運行以下步驟:

  1. 從輸入x開始計算z=W¹x¹+ b¹
  2. 對於中間結果z,再計算y=W²z¹+b²得到輸出y。

請注意,這裏用上標來表示你在哪一層,下標表示帶有一個向量或矩陣的位置。使用兩層而不是一層的方式在下圖中可以看到。

                        兩層的人工神經網絡,輸入神經元x連接到一組中間單元z,它們連接到輸出神經元y 

在這一點上,應該很清楚的是你不能綁定到任何要堆疊特定數量的層,你還需要使用更多。此外,你不一定一直使用sigmoid作爲激活函數,您有大量的激活函數可供選擇,我們將在下一章中介紹其中的一些函數。對於一個或多個數據點順序應用網絡中所有層的功能,通常被稱爲前向傳遞。它被稱爲前向的原因,是數據總是向前流動,從輸入到輸出,然後就不往回走。

有了這個符號,描述一個具有三層的常規前饋網絡就像下圖。 

爲了回顧你到目前爲止所學到的東西,讓我們在下面一個簡潔的列表中把提到的所有概念放出來:

  • 順序神經網絡是一種映射特徵的機制,或者輸入神經元,x去預測,或輸出神經元,y。通過順序地逐層堆疊簡單函數來實現這一點。
  • 一個層是將給定的輸入映射到輸出的東西。計算一批數據的一層輸出,我們稱爲前向傳遞。同樣,計算順序網絡的前向傳遞就是順序地計算從輸入層開始的每層的前向傳遞。
  • Sigmoid函數是一個激活函數,它接受實值神經元的向量作爲參數並激活它們,以至於它們可以映射到範圍[0,1]。您可以將接近1的值解釋爲激活。
  • 給定一個權重矩陣W和一個偏置項b,應用仿射非線性變換Wx+b形成一層。這種層通常被稱爲dense(密集)層或完全連接層,往下走,我們堅持稱它們爲dense(連接)層。
  • 根據實現的不同,dense(連接)層可能會或可能不會與激活函數一起;您可能會看到該層有s(Wx+b),而不僅僅是一個仿射線性變換信息。另一方面,一層只考慮激活函數是很常見的,您將在實現中也這樣做。
  • 一個前饋神經網絡是由具有激活的dense(連接)層組成的順序網絡,這種架構通常也被稱爲多層感知器,簡稱MLP。
  • 所有的既不是輸入也不是輸出的神經元叫做隱藏單元。相應的,輸入和輸出神經元有時被稱爲可見單元。直覺是隱藏單元是內部的網絡,而可見的單位是可以直接觀察到。這在某種程度上是一個延伸,因爲正常情況下你可以訪問系統的任何部分。因此,輸入和輸出兩層之間稱爲隱藏層:每個至少有兩個層的順序網絡至少有一個隱藏層。
  • 如果沒有另外說明,x將代表網絡的輸入,y表示輸出,有時用下標來表示你正在使用哪個樣本。將許多層堆疊起來,建立一個包含許多隱藏層的大網絡被稱爲深層神經網絡,因此這個名字叫做深度學習。

非順序神經網絡

到現在,你只學習到順序神經網絡,其中的層形成一個序列。在順序網絡中,從輸入開始,每個跟隨的隱藏層都正好有一個前驅和一個後繼,直到輸出層結束。這足以使你將深度學習應用到圍棋遊戲中。

總的來說,理論上神經網絡也允許任意的非順序結構。例如,在某些應用程序中,連接或添加兩個層的輸出(合併兩個或多個的先前層)是有意義的。在這種情況下,合併多個輸入然後產生一個輸出。

在其他應用程序中,將一個輸入分成幾個輸出是有用的。一般來說,一個層可以有多個輸入和輸出。我們在第11章和第12章分別介紹了多輸入和多輸出網絡。

具有n層的多層感知器被描述爲由權重集W=W¹,...,Wⁿ和偏差集b=b¹,...,bⁿ,以及爲每層選擇的激活函數集組成。但是一個重要的從數據中學習並更新參數的因素仍然缺少:損失函數和如何優化它們。

 5.4.我們的預測有多好?損失函數和優化器

 第5.3節定義瞭如何建立前饋神經網絡並通過它傳遞輸入數據,但您仍然不知道如何評估預測的質量。要做到這一點,你需要一個措施定義預測和實際的結果有多接近。

5.4.1 什麼是損失函數?

爲了用你的預測來量化你錯過了多少目標,我們引入了損失函數的概念,通常稱爲目標函數。假設你有一個帶有權重W、偏移量b和sigmoid激活函數的前饋網絡,對於給定的一組輸入特徵X1,...,Xk和相應的標籤Y1,...,Yk,使用網絡你可以計算預測Y1,...,YK。在這種情況下,損失函數的定義如下:

這裏,,是一個可微的函數。損失函數是一個平滑函數,可以將非負值表示爲多個(預測、標籤)對。一堆特徵和標籤的損失是樣本損失的總和。你的訓練目標是通過找到好的策略去調整參數,從而能夠儘量減少損失。

5.4.2.均方誤差

一個廣泛使用的損失函數是均方誤差(MSE)。雖然使用MSE對我們的用例並不理想,但它是最直觀的損失函數之一。你要測量預測值和實際結果有多接近,可以與所有觀察到的例子計算均方和平均。表示標籤,y=y1,...,yk表示預測,均方誤差定義如下:

在你已經看到了函數被應用之後,我們將介紹各種損失函數的優點和缺點。現在,讓我們在Python中實現均方誤差。 

import random
import numpy as np


class MSE:
    def __init__(self):
        pass

    # 計算均方誤差
    @staticmethod
    def loss_function(predictions,labels):
        diff = predictions - labels
        return 0.5*sum(diff*diff)[0]

    @staticmethod
    def loss_derivative(predictions,labels):
        return predictions - labels

請注意,您不僅實現了損失函數本身,而且還實現了導數:loss_derivate。這個導數是一個向量,是通過將預測向量減去標籤向量。接下來,您將看到像MSE導數這樣的在訓練神經網絡中起着至關重要的作用。

5.4.3.損失函數找到最小值

一組預測和標籤的損失函數爲您提供了關於模型參數調整情況的信息。損失越小,你的預測就越好,反之亦然。這個損失函數本身是您網絡參數的函數。在您的均方誤差實現中,您不直接提供權重,但是通過預測隱含地給出,因爲你可以使用權重去計算他們。

理論上,你從微積分中知道,要使損失最小化,你需要它的導數置爲0。此時,我們將參數集稱爲一種解決方案。計算一個函數的導數,並在一個特定的點上評估它,被稱爲計算梯度。現在您已經完成了在均方誤差實現中計算導數的第一步,但是有更重要的事。您的目標是顯式地計算網絡中所有權重和偏置項的梯度。

如果你需要複習微積分的基礎知識,一定要看看附錄A。圖5.8顯示了三維空間中的一個曲面。這個曲面可以解釋爲一個二維輸入的損失函數。平面的兩個軸代表你的權重,豎軸表示爲損失值。

 二維輸入(損耗面)的損失函數的一個例子。這個曲面在右下角的暗區周圍有一個最小值,可以通過求解損失函數的導數來計算

 5.4.4.梯度下降尋找最小值

直觀地說,當您計算給定點的函數梯度時,該梯度會指向最陡峭的上升方向。從一個損失函數、損失和一組參數開始,爲了找到這個函數的最小值,梯度下降算法是這樣的:

  1. 計算當前參數W的損失梯度Δ(重複計算每個權重下的損失函數的導數)。
  2. 通過減去梯度來更新W。我們把這個步驟稱爲沿着梯度。因爲梯度指向最陡峭的上升方向,減去它會引導你朝這個方向前進最多的下降。
  3. 重複這些過程,直到梯度爲0

因爲損失函數是非負的,而且它有一個最小值。它可能有很多,甚至無限多的最小值。例如,如果你考慮一個平面,它上的每一點都是最小的。

局部和全局的最小值

梯度下降達到零梯度的點被定義爲最小值。對於許多變量的可微函數,最小值的精確數學會使用有關函數曲率的信息。

隨着梯度下降,你最終會找到一個最小值;你可以跟隨函數的梯度,直到你找到一個零點梯度。有一個問題:你不知道這個最小值是局部的還是全局的最小值。你可能會被困在一個地方上,這個地方是函數所能接受的最小點,但其他的點可能具有較小的絕對值。圖5.8中的標記點是局部最小值,但顯然在這個表面存在更小的值。

我們爲解決這個問題所做的一切可能會使你感到震驚:我們將忽略它。在實踐中,梯度下降往往導致令人滿意的結果,因此在神經網絡損失函數的背景下,我們往往會忽略一個最小值是否是局部的還是全球的。我們通常在收斂之前甚至不會運行算法,而是在預定義的數數之後停止。 

下圖顯示了從上圖開始的梯度下降對損失面的作用,以及右上角標記點所指示的參數的選擇

在均方誤差實現中,您已經看到均方誤差損失函數的導數很容易,就是標籤和預測之間的區別。但要評估這樣一個導數,你必須首先要計算預測值。要查看所有參數的梯度,您必須評估和聚合訓練集中每個樣本的導數。假如你通常需要處理數百萬的數據樣本,這樣做實際上是不可行的。取而代之的,你將使用一種叫做隨機梯度下降的技術來近似計算梯度。

5.4.5.損失函數的隨機梯度下降

 要計算梯度,並將梯度下降應用於神經網絡,您必須要評估損失函數和在訓練集上每個點上網絡參數的導數,在大多數情況下太費時間了。取而代之的,我們將使用一種稱爲隨機梯度下降(SGD)的技術。要使用SGD,首先要從你的訓練集中選擇一些樣本,您稱之爲小訓練集。每份選擇的小訓練集都有一個固定的長度,我們稱之爲小訓練集個數。對於一個分類問題,像你正在處理的手寫數字問題,這是一個的做法,因此它的訓練集與標籤數量具有相同的數量級,從而確保每個標籤都能在一個小批量中表示。

對於一個給定的具有l層和輸入數據爲x₁,...xⁿ的小訓練集的前饋神經網絡,您可以計算您的神經網絡的前向傳遞,並計算該小訓練集的損失。對於本訓練集中的每個樣本xj,則可以選擇都去計算網絡中的每個參數來評估損失函數的梯度,在第i層的權重和偏移量梯度我們分別用∆jWi,∆jbi。

對於訓練集中的每一層和每一個樣本,您計算各自的梯度,並對使用參數以下更新規則:

您通過減去該批處理收到的累積錯誤來更新參數。這裏a>0表示學習率,這是在訓練網絡之前指定的一個實體。

如果你要一次總結所有的訓練樣本,你會得到更多關於梯度的精確信息。在梯度精度方面,使用小訓練集是一個折衷方案,但計算效率要高得多。我們稱這種方法爲隨機梯度下降,因爲小訓練集樣本是隨機選擇的。雖然在梯度下降中,你有一個接近局部最小值的理論保證,而在隨機梯度下降中。事實並非如此。圖5.10顯示了SGD(隨機梯度下降)的典型行爲。你的一些近似隨機梯度可能並不指向下降的方向,但是如果有足夠的迭代,你會通常接近接近最小值。

 優化器

計算(隨機)梯度是由微積分的基本原理來定義的,而使用梯度更新參數的方式卻不是。像SGD(隨機梯度下降)的更新規則這樣的技術稱爲優化器。

現在還有許多其他的優化器,以及更復雜的隨機梯度下降版本。我們在第7章中涵蓋了SGD(隨機梯度下降)的一些擴展。大多數擴展都是圍繞着適應隨着時間的推移的學習速率,或有更多的粒度去更新的個人權重。 

5.4.6.通過你的網絡反向傳播梯度

我們已經討論瞭如何使用隨機梯度下降來更新神經網絡的參數,但我們沒有解釋如何到達梯度。計算這些梯度的算法是稱爲反向傳播算法,我們已並在附錄B中詳細介紹。本節給出反向傳播背後的原理和實現前饋網絡的必要構建塊。

回想一下,在前饋網絡中,您通過計算一個又一個簡單的構建塊來實現數據的正向傳遞,由最後一層進行輸出網絡的預測,然後你可以根據對應的標籤去計算損失,而損失函數本身就是較簡單函數的組成,若要計算損失函數的導數,可以使用微積分的基本屬性:鏈式法則。這一規律粗略地說,組成函數的導數就是這些函數的導數的組成。因此,當您將輸入數據向前傳遞一層又一層時,你可以一層又一層地往回傳遞導數。因爲是通過網絡往回傳播導數,所以其名叫反向傳播。在圖5.11中,您可以看到一個擁有兩個dense層和sigmoid激活函數的前饋網絡反向傳播的行爲:

 

爲了指導你完成上圖的步驟,讓我們一步一步地做:

1.向前傳遞訓練數據。在此步驟中,您獲得了一個輸入數據樣本x並將其沿着網絡傳遞,從而獲得預測,詳情如下:

  • 你計算仿線性部分:Wx+b。
  • 將Sigmoid函數σ(X)應用於結果。
  • 重複這兩個步驟,直到到達輸出層。我們在這個例子中選擇了兩個層,但是層數並不重要。

2.損失函數評估。在此步驟中,您將樣本x的標籤取出來,並通過與你的預測值進行比較得出損失值。在本例中,您選擇了均方誤差作爲損失函數。

3.反向傳遞誤差.。在這一步中,您將獲取損失值並通過網絡往回傳遞。你這樣做是按層計算導數,因爲符合鏈規則,所以是可行的。在一個方向上沿着網絡向前傳遞輸入數據,向後傳遞反饋的錯誤。

  • 你反向傳遞錯誤項,或Δ。
  • 首先,您計算損失函數的導數,它將是您的初始Δ。然後不斷往回傳遞
  • 計算Sigmoid導數,這只是簡單的σ(1-σ)。若要將Δ傳遞到下一層,則可以進行計算乘法:σ(1-σ)·Δ。
  • 你的仿線性變換Wx+b相對簡單的。爲了傳遞Δ,你要計算
  • 這兩個步驟會重複,直到你到達網絡的第一層。

4.用梯度信息更新權重。在最後一步中,您使用一路上計算的Δ來更新您的網絡參數(權重和偏移量)。

  • sigmoid函數沒有任何參數,所以你不需要做什麼。
  • 每層中的偏移項b的更新量Δb只是簡單Δ
  • 一層中權重W的更新量ΔW應爲Δ*x(在與Δ相乘前x需要轉置)。
  • 請注意,我們首先說x是一個單一的樣本。我們討論過的每件事都離不開小樣本。如果x表示一小批樣本(x是輸入向量中任何一列的矩陣),那麼向前和向後傳遞的計算看起來完全相同。

既然你擁有構建和運行一個前饋網絡的所有數學知識,讓我們應用您在理論層面上學到的東西,從零開始構建一個神經網絡。

5.5 使用Python一步接一步地訓練神經網絡

前一節涵蓋了許多理論基礎,但在概念層面上,你只學到了幾個基本概念。對於我們的實現,您只需關注三件事:一個Layer類,一個SequentialNetwork類(它是通過一個接一個地添加Layer對象來構建的),以及一個需要反向傳播的Loss類。在您將加載和檢測手寫數字數據,並將您的網絡實現應用於它之後,這三個類接下來會被使用。圖5.12顯示了這些Python類是如何組合在一起實現前一節描述的前向和後向傳遞。

圖5.12 用Python實現的前饋網絡類圖。順序網絡包含多個層實例。每個層實現一個數學函數及其導數。前向和後向的方法分別實現前向傳遞和後向傳遞。一個損失實例計算你的預測和您的訓練數據之間的誤差。 

5.5.1.用Python實現神經網絡層 

要從一個一般的Layer類開始,請注意,正如我們前面討論過的那樣,Layer不僅有一個處理輸入數據(前向傳遞)的方法,而且還有一個反向傳遞錯誤的方法。爲了在反向傳遞過程上不重新計算激活值,保持兩次進出數據的狀態是必要的。既然這麼說了,Layer類的初始化應該是直截了當的。現在,您將開始創建一個Layer模塊;在本章後面,您將使用此模塊中的組件來構建神經網絡。 

import numpy as np


#堆疊層來構建神經網絡
class Layer: 
    def __init__(self): 
        self.params = []
        # 該層的前驅
        self.previous = None 
        # 該層的後繼
        self.next = None 
        # 每一層在前向傳遞過程中都會輸入和輸出數據
        self.input_data = None
        self.output_data = None 
        # 類似地,每一層在後向傳遞過程中輸入和輸出增量
        self.input_delta = None 
        self.output_delta = None

一層有一個參數列表,並存儲其當前的輸入和輸出數據,以及向後傳遞的相應的輸入和輸出增量。

另外,因爲你關心的是在順序神經網絡中,每一層都有一個前驅和後繼。因此繼續定義,添加以下內容。

# 在順序網絡中將一個層連接到它的直接鄰居。
def connect(self, layer): 
    self.previous = layer
    layer.next = self

接下來,你將讓抽象Layer類中的前向和後向傳遞方法佔位,而讓子類去實現這些方法。

# 每個層的實現都必須提供一個功能來向前輸入數據
def forward(self): 
     raise NotImplementedError

# input_data是爲第一層保留的;所有其他層都從前一層的輸出中獲得他們的輸入。
def get_forward_input(self): 
    if self.previous is not None: 
       return self.previous.output_data
    else:
       return self.input_data 

# 層必須實現錯誤項的反向傳播——一種通過網絡反饋傳遞錯誤的方法
def backward(self): 
    raise NotImplementedError 

# 輸入增量保留在最後一層;所有其他層都從它們的後繼那裏獲得錯誤項。
def get_backward_input(self): 
    if self.next is not None: 
       return self.next.output_delta
    else:
       return self.input_delta

# 每個小樣本集計算和累積增量,然後需要重置這些增量。
def clear_deltas(self): 
    pass 

# 根據當前增量更新圖層參數,使用指定的學習率
def update_params(self, learning_rate): 
    pass
 
# 層實現可以打印其屬性
def describe(self): 
    raise NotImplementedError

作爲輔助函數,您提供了get_forward_input和get_backward_input,它們只是爲各自的傳遞檢索輸入,但要特別注意輸入和輸出神經元。最重要的是,你實現了一個clear_deltas方法,在將增量積累到小批次之後,定期重置增量,還有update_params,該方法負責在網絡使用這層之後更新該層的參數。

請注意,作爲功能的最後一部分,您爲一個層添加了一個方法來打印自己的描述,爲了方便起見,添加了該方法可以更容易的獲知你網絡的樣子

5.5.2.神經網絡中的激活層

接下來,您將提供您的第一層,激活層。你將使用你已經實現的Sigmoid函數。爲了做反向傳播,你還需要它的導數,這也是很容易實現。 

def sigmoid_prime_double(x): 
      return sigmoid_double(x) * (1 ­- sigmoid_double(x))

def sigmoid_prime(z):
    return np.vectorize(sigmoid_prime_double)(z)

請注意,對於Sigmoid本身,您提供了導數的標量版本和向量版本。現在,要定義一個使用sigmoid函數作爲內置激活的ActivationLayer類,而Sigmoid函數沒有任何參數,所以您不需要更新任何參數

# 這個激活層利用sigmoid函數來激活神經元
class ActivationLayer(Layer): 
   def __init__(self, input_dim): 
      super(ActivationLayer, self).__init__()
      self.input_dim = input_dim
      self.output_dim = input_dim

   # 向前傳遞只是將Sigmoid應用於輸入數據
   def forward(self): 
       data = self.get_forward_input() 
       self.output_data = sigmoid(data) 

   def backward(self): 
       delta = self.get_backward_input()
       data = self.get_forward_input() 
       # 往回傳遞時的輸出是用Sigmoid函數的導數與增量相乘
       self.output_delta = delta * sigmoid_prime(data) 
       
   def describe(self):
       print("|­­ " + self.__class__.__name__) print(" |­­ dimensions: ({},{})".format(self.input_dim, self.output_dim))

對於這一層,向後傳遞的只是將該層當前的元素增量與sigmoid導數乘積:σ(X)·(1-σ(X))·Δ。

5.5.3.Python中的Dense層作爲前饋網絡的構建塊 

DenseLayer,是更復雜的層,也是您將在本章中實現的最後一個層。初始化這個層需要還有幾個變量,權重矩陣、偏置項,以及它們各自的梯度。

class DenseLayer(Layer): 
    # Dense層具有輸入和輸出尺寸
    def __init__(self, input_dim, output_dim): 
        super(DenseLayer, self).__init__() 
        self.input_dim = input_dim 
        self.output_dim = output_dim 

        # 隨機初始化權重矩陣和偏置向量
        self.weight = np.random.randn(output_dim, input_dim) 
        self.bias = np.random.randn(output_dim, 1)
        
        # 層參數由權重和偏置項組成
        self.params = [self.weight, self.bias] 

        # 權重和偏差的∆設置爲0
        self.delta_w = np.zeros(self.weight.shape) 
        self.delta_b = np.zeros(self.bias.shape)

注意,您隨機初始化了W和b。有許多方法可以初始化神經網絡的權重。隨機初始化是一個可以接受的方式,但其實還有許多更復雜的方式去初始化參數,可以更加準確地反映輸入數據的結構。

參數初始化是優化的一個起點

初始化參數是一個有趣的主題,我們將在第6章中討論一些其他的初始化技術。

現在,只要記住,初始化將影響你的學習行爲。如果你想到圖5.10的損失表面中,參數的初始化將意味着選擇一個優化的起點;您可以很容易地想象到,圖5.10損失面上的SGD的不同起點可能會導致不同的結果,這使得初始化成爲神經網絡研究的一個重要課題。現在,一個Dense層的向前傳遞是直接向前的。

# Dense層的前向傳遞是由權重和偏差定義的輸入數據的仿線性變換
def forward(self):
    data = self.get_forward_input()
    self.output_data = np.dot(self.weight,data)+self.bias

至於向後傳遞,請記住,要計算此層的增量,只需將W轉置並乘以傳入的增量:。W和b的梯度也很容易計算d:ΔW=Δyt,Δb=Δ,其中y表示該層的輸入(用您當前使用的數據計算)。

def backward(self): 
    # 對於後向傳遞,首先獲取輸入數據和增量
    data = self.get_forward_input() 
    delta = self.get_backward_input() 
 
    # 當前的增量被添加到偏置增量中
    self.delta_b += delta 

    # 然後你把這個加到權重增量中
    self.delta_w += np.dot(delta, data.transpose()) 

    # 後向傳遞是通過將輸出增量傳遞到前一層來完成的
    self.output_delta = np.dot(self.weight.transpose(), delta) 

根據您爲網絡指定的學習速率,通過累積增量來給出此層的更新規則。

# 使用權重和偏置增量,您可以更新具有梯度下降的模型參數
def update_params(self, rate): 
     self.weight ­= rate * self.delta_w 
     self.bias ­= rate * self.delta_b 

# 更新參數後,應重置所有增量
def clear_deltas(self): 
    self.delta_w = np.zeros(self.weight.shape)
    self.delta_b = np.zeros(self.bias.shape)

# 一個Dense層可以用它的輸入和輸出尺寸來描述
def describe(self): 
    print("|­­­ " + self.__class__.__name__) 
    print(" |­­ dimensions: ({},{})" .format(self.input_dim, self.output_dim))

5.5.4.用Python做順序神經網絡

把層作爲一個網絡的構建塊,讓我們轉向網絡本身。通過添加一系列空的層列表來初始化一個順序神經網絡,然後讓它使用MSE作爲損失函數。 

class SequentialNetwork: 
    def __init__(self, loss=None): 
         print("Initialize Network...") 
         self.layers = [] 
         if loss is None: 
             self.loss = MSE()

接下來,添加逐個添加層的功能

# 每當你添加一個層,你就把它連接到它的前驅,然後讓它描述自己。
def add(self, layer): 
    self.layers.append(layer) 
    layer.describe() 
    # -1表示最後一個,即加入的層,-2是當前層的前驅,所以將兩個連起
    # connect在Layer類中實現
    if len(self.layers) > 1: 
        self.layers[-­1].connect(self.layers[-­2])

網絡實現的核心是訓練方法。您使用小訓練集作爲輸入:您對訓練數據進行洗牌,並將其拆分爲大小爲mini_batch_size的訓練集。爲了訓練你的網絡,你需要一個接一個地餵它。爲了提高學習能力,您將多次分批向網絡提供您的訓練數據。我們說我們訓練它要經歷多少輪次。如果提供了test_data,則在每一輪之後評估網絡性能。

def train(self, training_data, epochs, mini_batch_size, learning_rate, test_data=None):
      n = len(training_data) 
      # 爲了訓練你的網絡,你傳遞數據的次數要和輪數一樣多
      for epoch in range(epochs):
          # 打亂訓練數據並創建小訓練集
          random.shuffle(training_data)
          mini_batches = [ 
               training_data[k:k + mini_batch_size] for k in range(0, n, mini_batch_size)
          ]
          # 對於每一個小訓練集,你都要訓練你的網絡
          for mini_batch in mini_batches: 
               self.train_batch(mini_batch, learning_rate) 
          if test_data: 
               n_test = len(test_data) 
               # 如果您提供了測試數據,則在每輪之後對您的網絡進行評估。
               print("Epoch {0}: {1} / {2}" .format(epoch, self.evaluate(test_data),n_test)) 
          else:
               print("Epoch {0} complete".format(epoch))

現在,您的train_batch計算此小訓練集的前向和後向傳遞,並在之後更新參數

def train_batch(self,mini_batch,learning_rate):
       # 要在一個小訓練上訓練網絡,您需要計算前向和後向傳遞
       self.forward_backward(mini_batch)
       # 更新模型參數
       self.update(mini_batch,learning_rate)

這兩個步驟,update和forward_backword,計算如下

# 一種常見的技術是通過小訓練集大小來規範學習率。
def update(self, mini_batch, learning_rate): 
    learning_rate = learning_rate / len(mini_batch) 
    for layer in self.layers: 
        # 更新該層所有參數
        layer.update_params(learning_rate) 
    for layer in self.layers:
        # 清除每層所有增量
        layer.clear_deltas() 

# 對於小訓練集中的每個樣本,要逐層向前提供特徵
def forward_backward(self, mini_batch): 
    for x, y in mini_batch: 
       self.layers[0].input_data = x 
    for layer in self.layers: 
        layer.forward() 

    # 計算輸出數據的損失導數。
    self.layers[­-1].input_delta = \ self.loss.loss_derivative(self.layers[­-1].output_data, y) 

    # 誤差項的逐層反向傳播
    for layer in reversed(self.layers): 
         layer.backward()

實施是直截了當的,但有幾個值得注意的點需要觀察。首先,您將學習速率標準化爲您的小訓練集大小,以保持較小的更新。第二,在通過反向遍歷層計算完整的向後傳遞之前,要計算網絡輸出的損失導數,它是作爲向後傳遞的第一個輸入增量。

SequentialNet work剩下未實現的一部分涉及模型性能和評估。要在測試數據上評估您的網絡,您需要通過您的網絡將這些數據向前傳遞,這是正是single_forward所做的。評估發生在evaluate方法中,它會返回正確預測結果的數量以評估準確性。

# 向前傳遞單個樣本並返回結果
def single_forward(self, x): 
     self.layers[0].input_data = x 
     for layer in self.layers: 
         layer.forward() 
     return self.layers[-­1].output_data 

# 評估測試時的準確性
def evaluate(self, test_data): 
     test_results = [( np.argmax(self.single_forward(x)), np.argmax(y) ) for (x, y) in test_data]
      return sum(int(x == y) for (x, y) in test_results)

5.5.5.應用你的網絡手寫數字分類

 在實現了前饋網絡之後,讓我們回到我們預測MNIST數據集手寫數字的初始用例。在導入剛剛構建的必要類之後,我們加載MNIST數據,初始化網絡,向其添加層,然後使用數據訓練和評估網絡。

要建立一個網絡,請記住你的輸入維度是784,而你的輸出維度是10。您選擇三個dense層,分別具有輸出維度392、196和10,並在每個層之後添加Sigmoid激活。對於每一個新的dense層,你都有效地將層容量分成兩半。層的大小和層數是該網絡的參數,您已經選擇了這些值來設置網絡體系結構。

from dlgo.nn 
import load_mnist
from dlgo.nn import network 
from dlgo.nn.layers import DenseLayer, ActivationLayer 


# 加載訓練數據
training_data, test_data = load_mnist.load_data() 
# 初始化順序神經網絡
net = network.SequentialNetwork() 
# 逐層添加dense層和激活層
net.add(DenseLayer(784, 392)) 
net.add(ActivationLayer(392))
net.add(DenseLayer(392, 196)) 
net.add(ActivationLayer(196))
# 最後一層大小是10,要產生預測 
net.add(DenseLayer(196, 10)) 
net.add(ActivationLayer(10))

您通過調用train方法來訓練網絡的數據。您運行10輪的訓練,並將學習率設置爲3.0。作爲小訓練集,您選擇10個類。如果您將訓練數據完全打算,那麼在大多數批次中,每個類都將被表示,從而得到良好的隨機梯度。

net.train(training_data, epochs=10, mini_batch_size=10, learning_rate=3.0, test_data=test_data)

現在可以運行了

你得到的每輪的數字在這裏都不重要,因爲事實上結果高度依賴於權重的初始化。但是值得注意的是,你經常在不到10輪時就具有95%以上的精度。這已經是一個相當大的成就,特別是考慮到你完全從零開始這樣做。特別要提的是,這種模型的表現要你從本章開始的天真模型要好的多。不過,你可以做得更好。
 
請注意,對於您所研究的用例,您完全忽略了輸入圖像的空間結構而把它們當作向量。但應該清楚的是,給定像素的鄰域是應該使用的重要信息。最終,你想回到圍棋的遊戲,如果你看到了第二章和第三章,那麼就會明白(一串)棋子的鄰域是多麼重要。
 
在下一章中,您將看到如何構建一種更適合於檢測空間數據的神經網絡,如圖像或圍棋盤。這將使您更接近於在第7章中開發一個圍棋AI。
 

5.6.總結

  • 順序神經網絡是一種簡單的人工神經網絡,是由一層線性堆疊構建的。你可以將神經網絡應用於各種各樣的機器學習問題,包括圖像識別。
  • 前饋網絡是一個連續的網絡由具有激活函數的dense層組成的。
  • 損失函數評估我們預測的質量。均方誤差是實際應用中最常見的損失函數之一。一個損失函數給你一個嚴格的方法來量化你的模型的準確性。
  • 梯度下降是一種損失最小化的算法。梯度下降包括沿着最陡峭的斜坡進行。在機器學習中,你使用梯度下降來找到最小損失的模型權重。
  • 隨機梯度下降是梯度下降算法的一種變化。隨機梯度在下降時,您將計算您的訓練集的一個小子集上的梯度,稱爲小訓練集,然後根據每個小訓練集去更新網絡權重。隨機梯度下降通常比大型訓練集上的常規梯度下降快得多。
  • 使用順序神經網絡,您可以使用反向傳播算法有效地計算梯度。反向傳播和小訓練集的結合使得其在大數據集上訓練也變得實用

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章