魚書(深度學(xué)習(xí)入門)第三章:神經(jīng)網(wǎng)絡(luò)
一、從感知機到神經(jīng)網(wǎng)絡(luò)
? ? 神經(jīng)網(wǎng)絡(luò)的結(jié)構(gòu)如下圖所示。我們把最左邊的一列稱為輸入層,最右邊的一列稱為輸出層,中間的一列稱為中間層。中間層有時也稱為隱藏層。“隱藏”一詞的意思是,隱藏層的神經(jīng)元(和輸入層、輸出層不同)肉眼看不見。

????在上一章感知器中,我們有以下的數(shù)學(xué)表達(dá)式。

????我們將其改寫為以下形式:


????當(dāng)我們考慮h(x)何時等于0或者1的情況時,我們便引入了激活函數(shù)這個概念。剛才登場的h(x)函數(shù)會將輸入信號的總和轉(zhuǎn)換為輸出信號,這種函數(shù)一般稱為激活函數(shù)(activation function)。如“激活”一詞所示,激活函數(shù)的作用在于決定如何來激活輸入信號的總和。當(dāng)我們在神經(jīng)元示意圖中加入激活函數(shù)時,便如下圖所示。

????下面我們介紹幾個常用的激活函數(shù)。
????1.階躍函數(shù)
????當(dāng)一個激活函數(shù)以閾值為界,一旦輸入超過閾值,就切換輸出。 這樣的函數(shù)稱為“階躍函數(shù)”。
????下面給出的代碼可以繪制出階躍函數(shù)的圖形。
示例代碼:
import numpy as np
import matplotlib.pylab as plt
def step_function(x): #展示階躍函數(shù)
? ? return np.array(x>0,dtype=np.int_)
x=np.arange(-5.0,5.0,0.1)
y=step_function(x)
plt.plot(x,y)
plt.ylim(-0.1,1.1) #指定y軸的范圍
plt.show()
圖形如下:

????要在代碼中實現(xiàn)階躍函數(shù),使用以下代碼即可。
def step_function(x):?
????y = x > 0?
????return y.astype(np.int_)
????需要注意的是,對NumPy數(shù)組進(jìn)行不等號運算后,數(shù)組的各個元素都會進(jìn)行不等號運算,生成一個布爾型數(shù)組。這里,數(shù)組x中大于0的元素被轉(zhuǎn)換為True,小于等于0的元素被轉(zhuǎn)換為False,從而生成一個新的數(shù)組y??梢杂胊stype()方法轉(zhuǎn)換NumPy數(shù)組的類型。astype()方法通過參數(shù)指定期望的類型,這個例子中是np.int型。Python中將布爾型轉(zhuǎn)換為int型后,True會轉(zhuǎn)換為1,F(xiàn)alse會轉(zhuǎn)換為0。
????2.sigmoid函數(shù)
????神經(jīng)網(wǎng)絡(luò)中經(jīng)常使用的一個激活函數(shù)就是下式表示的sigmoid函數(shù)(sigmoid function)。實際上,上一章介紹的感知機和接下來要介紹的神經(jīng)網(wǎng)絡(luò)的主要區(qū)別就在于這個激活函數(shù)。其他方面,比如神經(jīng)元的多層連接的構(gòu)造、信號的傳遞方法等,基本上和感知機是一樣的。

????下面給出繪制sigmoid函數(shù)的代碼。
示例代碼:
import numpy as np
import matplotlib.pylab as plt
def sigmoid(x): #展示sigmoid函數(shù)
? ? return 1/(1+np.exp(-x)) #此處numpy有對應(yīng)的廣播機制
x=np.arange(-5.0,5.0,0.1)
y=sigmoid(x)
plt.plot(x,y)
plt.ylim(-0.1,1.1) #指定y軸的范圍
plt.show()
其圖形如下:

????使用以下函數(shù)即可在代碼中實現(xiàn)sigmoid函數(shù):
def sigmoid(x): #sigmoid函數(shù)
? ? return 1/(1+np.exp(-x)) #此處numpy有對應(yīng)的廣播機制
3.階躍函數(shù)與sigmoid函數(shù)的異同
????首先注意到的是“平滑性”的不同。sigmoid函數(shù)是一條平滑的曲線,輸出隨著輸入發(fā)生連續(xù)性的變化。而階躍函數(shù)以0為界,輸出發(fā)生急劇性的變化。sigmoid函數(shù)的平滑性對神經(jīng)網(wǎng)絡(luò)的學(xué)習(xí)具有重要意義。
????另一個不同點是,相對于階躍函數(shù)只能返回0或1,sigmoid函數(shù)可以返回連續(xù)的實數(shù)。也就是說,感知機中神經(jīng)元之間流動的是0或1的二元信號,而神經(jīng)網(wǎng)絡(luò)中流動的是連續(xù)的實數(shù)值信號。
????接著說一下階躍函數(shù)和sigmoid函數(shù)的共同性質(zhì)。階躍函數(shù)和sigmoid函數(shù)雖然在平滑性上有差異,但是如果從宏觀視角看,可以發(fā)現(xiàn)它們具有相似的形狀。實際上,兩者的結(jié)構(gòu)均是“輸入小時,輸出接近0(為0);隨著輸入增大,輸出向1靠近(變成1)”。也就是說,當(dāng)輸入信號為重要信息時,階躍函數(shù)和sigmoid函數(shù)都會輸出較大的值;當(dāng)輸入信號為不重要的信息時,兩者都輸出較小的值。還有一個共同點是,不管輸入信號有多小,或者有多大,輸出信號的值都在0到1之間。
????階躍函數(shù)和sigmoid函數(shù)還有其他共同點,就是兩者均為非線性函數(shù)。 sigmoid函數(shù)是一條曲線,階躍函數(shù)是一條像階梯一樣的折線,兩者都屬于 非線性的函數(shù)。
? ? 一個重要的性質(zhì)是:神經(jīng)網(wǎng)絡(luò)的激活函數(shù)必須使用非線性函數(shù)。因為使用線性函數(shù)的話,加深神經(jīng)網(wǎng)絡(luò)的層數(shù)就沒有意義了。線性函數(shù)的問題在于,不管如何加深層數(shù),總是存在與之等效的“無隱藏層的神經(jīng)網(wǎng)絡(luò)”。
????4.ReLU函數(shù)
????在神經(jīng)網(wǎng)絡(luò)發(fā)展的歷史上,sigmoid函數(shù)很早就開始被使用了,而最近則主要使用ReLU(Rectified Linear Unit)函數(shù)。ReLU函數(shù)在輸入大于0時,直接輸出該值;在輸入小于等于0時,輸出0。其可以表達(dá)為以下式子與圖形。

????

????其函數(shù)實現(xiàn)如下:
def relu(x): #展示EeLU函數(shù)
? ? return np.maximum(0,x)
????這里使用了NumPy的maximum函數(shù)。maximum函數(shù)會從輸入的數(shù)值中選擇較大的那個值進(jìn)行輸出。
二、NumPy多維數(shù)組的運算
????簡單地講,多維數(shù)組就是“數(shù)字的集合”,數(shù)字排成一列的集合、排成長方形的集合、排成三維狀或者(更加一般化的)N維狀的集合都稱為多維數(shù)組。

????上面的代碼生成了一個3 × 2的數(shù)組B。3 × 2的數(shù)組表示第一個維度有3個元素, 第二個維度有2個元素。由示例可知,數(shù)組的維數(shù)可以通過np.dim()函數(shù)獲得,數(shù)組的形狀可以通過實例變量shape獲得。二維數(shù)組也稱為矩陣(matrix)。數(shù)組的橫向排列稱為行(row),縱向排列稱為列(column)。
????矩陣的乘法這里就不贅述了,但是與線性代數(shù)中的運算法則相同,在矩陣的乘積運算中,對應(yīng)維度的元素個數(shù)要保持一致,如下圖所示:

????下面介紹使用Numpy快速的計算矩陣的內(nèi)積。
示例代碼:
X=np.array([1,2]) #numpy矩陣點乘
print(X.shape)
W=np.array([[1,3,5],[2,4,6]])
print(W)
print(W.shape)
Y=np.dot(X,W)
print(Y)
????如上所示,使用np.dot(多維數(shù)組的點積),可以一次性計算出Y 的結(jié)果。如果不使用np.dot,就必須單獨計算Y 的每一個元素(或者說必須使用for語句),非常麻煩。
三、三層神經(jīng)網(wǎng)絡(luò)的實現(xiàn)
????1.符號確認(rèn)
????在介紹神經(jīng)網(wǎng)絡(luò)的實現(xiàn)之前,我們先引入幾個符號。下面是權(quán)重符號表示的圖示:

????2.各層之間信號的傳遞
????下圖展示了從輸入層到第1層的信號傳遞。

????使用矩陣表示,激活函數(shù)使用sigmoid函數(shù)的代碼如下:
def sigmoid(x): #sigmoid函數(shù)
? ? return 1/(1+np.exp(-x)) #此處numpy有對應(yīng)的廣播機制
X=np.array([1.0,0.5]) #第一層神經(jīng)網(wǎng)絡(luò)
W1=np.array([[0.1,0.3,0.5],[0.2,0.4,0.6]])
B1=np.array([0.1,0.2,0.3])
print(W1.shape) #(2, 3)
print(X.shape) #(2,)
print(B1.shape) #(3,)
A1=np.dot(X,W1)+B1
Z1=sigmoid(A1)
print(A1) #[0.3 0.7 1.1]
print(Z1) #[0.57444252 0.66818777 0.75026011]
????只要將第一層的輸出結(jié)果作為第二層的輸入,即可很方便的實現(xiàn)第1層到第2層的信號傳遞,如下圖所示。

代碼如下:
W2=np.array([[0.1,0.4],[0.2,0.5],[0.3,0.6]])#第二層神經(jīng)網(wǎng)絡(luò)
B2=np.array([0.1,0.2])
print(Z1.shape)
print(W2.shape)
print(B2.shape)
A2=np.dot(Z1,W2)+B2
Z2=sigmoid(A2)
print(A2) #[0.51615984 1.21402696]
print(Z2) #[0.62624937 0.7710107 ]
????最后是第2層到輸出層的信號傳遞。不過,最后的激活函數(shù)和之前的隱藏層有所不同。我們定義了identity_function()函數(shù)(也稱為“恒等函數(shù)”),并將其作為輸出層的激活函數(shù)。恒等函數(shù)會將輸入按原樣輸出。另外,圖中輸出層的激活函數(shù)用σ()表示,不同于隱藏層的激活函數(shù)h()。如下圖所示:

其代碼如下:
def identity_function(x): #輸出層的激活函數(shù)為恒等函數(shù)
? ? return x
W3=np.array([[0.1,0.3],[0.2,0.4]]) #第三層神經(jīng)網(wǎng)絡(luò)
B3=np.array([0.1,0.2])
A3=np.dot(Z2,W3)+B3
Y=identity_function(A3)
print(A3) #[0.31682708 0.69627909]
print(Y)? #[0.31682708 0.69627909]
????3.代碼實現(xiàn)小結(jié)
????我們已經(jīng)介紹完了3層神經(jīng)網(wǎng)絡(luò)的實現(xiàn)。現(xiàn)在我們把之前的代碼實現(xiàn)全部整理一下。按照慣例,我們權(quán)重記為大寫字母W1,其他的(偏置或中間結(jié)果等)都用小寫字母表示。其代碼如下:
def sigmoid(x): #sigmoid函數(shù)
? ? return 1/(1+np.exp(-x))
def identity_function(x): #輸出層的激活函數(shù)為恒等函數(shù)
? ? return x
def init_network():
? ? network={}
? ? network['W1']=np.array([[0.1,0.3,0.5],[0.2,0.4,0.6]])
? ? network['b1']=np.array([0.1,0.2,0.3])
? ? network['W2']=np.array([[0.1,0.4],[0.2,0.5],[0.3,0.6]])
? ? network['b2']=np.array([0.1,0.2])
? ? network['W3']=np.array([[0.1,0.3],[0.2,0.4]])
? ? network['b3']=np.array([0.1,0.2])
? ? return network
def forward(network,x):
? ? W1,W2,W3=network['W1'],network['W2'],network['W3']
? ? b1,b2,b3=network['b1'],network['b2'],network['b3']
? ? a1=np.dot(x,W1)+b1
? ? z1=sigmoid(a1)
? ? a2=np.dot(z1,W2)+b2
? ? z2=sigmoid(a2)
? ? a3=np.dot(z2,W3)+b3
? ? y=identity_function(a3)
? ? return y
network=init_network()
x=np.array([1.0,0.5])
y=forward(network,x)
print(y)
????這里定義了init_network()和forward()函數(shù)。init_network()函數(shù)會進(jìn)行權(quán)重和偏置的初始化,并將它們保存在字典變量network中。這個字典變量network中保存了每一層所需的參數(shù)(權(quán)重和偏置)。forward()函數(shù)中則封裝了將輸入信號轉(zhuǎn)換為輸出信號的處理過程。
四、輸出層的設(shè)計
????神經(jīng)網(wǎng)絡(luò)可以用在分類問題和回歸問題上,不過需要根據(jù)情況改變輸出層的激活函數(shù)?;貧w問題可以使用恒等函數(shù),二元分類問題可以使用 sigmoid函數(shù),多元分類問題可以使用 softmax函數(shù)。一般而言,回歸問題用恒等函數(shù),分類問題用softmax函數(shù)。
????1.恒等函數(shù)與softmax函數(shù)
????恒等函數(shù)很好理解,如下圖所示:

????而softmax函數(shù)的分子是輸入信號ak的指數(shù)函數(shù),分母是所有輸入信號的指數(shù)函數(shù)的和??梢杂孟率奖硎荆?/p>

由于其利用到了上層的所有輸入,其信號傳遞圖如下所示:

????softmax函數(shù)的代碼實現(xiàn)如下:
def softmax(a):
? ? exp_a=np.exp(a)?
? ? sum_exp_a=np.sum(exp_a)
? ? y=exp_a/sum_exp_a
? ? return y
????2.實現(xiàn) softmax函數(shù)時的注意事項
?????softmax函數(shù)的實現(xiàn)在計算機的運算上有一定的缺陷。這個缺陷就是溢出問題。softmax函數(shù)的實現(xiàn)中要進(jìn)行指數(shù)函數(shù)的運算,但是此時指數(shù)函數(shù)的值很容易變得非常大。我們可以使用下式進(jìn)行改進(jìn):

????????在進(jìn)行softmax的指數(shù)函數(shù)的運算時,加上(或者減去)某個常數(shù)并不會改變運算的結(jié)果。這里的C’可以使用任何值,但是為了防止溢出,一般會使用輸入信號中的最大值。其代碼實現(xiàn)如下所示:
def softmax(a):
? ? c=np.max(a)
? ? exp_a=np.exp(a-c) #溢出對策
? ? sum_exp_a=np.sum(exp_a)
? ? y=exp_a/sum_exp_a
? ? return y
????3.softmax函數(shù)的特征
????softmax函數(shù)的輸出是0.0到1.0之間的實數(shù)。并且,softmax函數(shù)的輸出值的總和是1。輸出總和為1是softmax函數(shù)的一個重要性質(zhì)。正因為有了這個性質(zhì),我們才可以把softmax函數(shù)的輸出解釋為“概率”。這里需要注意的是,即便使用了softmax函數(shù),各個元素之間的大小關(guān)系也不會改變。這是因為指數(shù)函數(shù)(y = exp(x))是單調(diào)遞增函數(shù)。一般而言,神經(jīng)網(wǎng)絡(luò)只把輸出值最大的神經(jīng)元所對應(yīng)的類別作為識別結(jié)果。并且,即便使用softmax函數(shù),輸出值最大的神經(jīng)元的位置也不會變。
????4.輸出層的神經(jīng)元數(shù)量
????輸出層的神經(jīng)元數(shù)量需要根據(jù)待解決的問題來決定。對于分類問題,輸出層的神經(jīng)元數(shù)量一般設(shè)定為類別的數(shù)量。對于手寫數(shù)字識別問題,其輸出可表示為下圖:
