深度學習(Deep Learning)基礎概念1:神經網路基礎介紹及一層神經網路的python實現

此專欄文章隨時更新編輯,如果你看到的文章還沒寫完,那麼多半是作者正在更新或者上一次沒有更新完,請耐心等待,正常的頻率是每天更新一篇文章。

該文章是「深度學習(Deep Learning)」系列文章的第一部分,首發於知乎的專欄「深度學習+自然語言處理(NLP)」。

該系列文章的目的在於理清利用深度學習進行自然語言處理的一些基本概念。

以下是相關文章的鏈接地址:

機器學習中的Python(一):Python基礎數據類型、容器、函數和類的介紹

機器學習中的Python(二):Numpy數組、索引、數據類型、運算、廣播的介紹

深度學習(Deep Learning)基礎概念(一):神經網路基礎(Neural Network Basics)介紹及其python實現

自然語言處理(NLP)基礎概念(一):詞向量及其生成方法介紹

自然語言處理(NLP)基礎概念(二):Softmax介紹及其python實現

自然語言處理(NLP)基礎概念(三):GloVe模型-全球向量用於詞表示

自然語言處理(NLP)基礎概念(四):詞向量的評估方法介紹-內部評估 (Intrinsic Evaluation)

機器學習中的線性代數系列(一):基本概念表達和矩陣乘法

機器學習中的線性代數系列(二):矩陣的操作和性質

機器學習中的線性代數系列(三):矩陣微積分

也歡迎關注專欄:「深度學習+自然語言處理(NLP)」

====================================================================

按照慣例先放出文章目錄:

  1. 推導 sigmoid函數的梯度
  2. 推導交叉熵函數關於softmax向量的梯度
  3. 推導包含一個隱藏層神經網路的梯度
  4. 神經網路參數的討論
  5. sigmoid函數及其梯度的python實現
  6. 梯度檢查器的python實現
  7. 神經網路的正向和反向傳遞及其python實現

====================================================================

下面開始正文:

1. 推導sigmoid函數的梯度

我們想要推導sigmoid函數的梯度,並證明它的梯度的表達式可以用sigmoid函數表示(比較拗口,往下看)。

首先,sigmoid函數如下:

sigma(x) = frac{1}{1+e^{-x}}

在我么開始求梯度之前,先弄清楚一個更重要的終極問題:sigmoid函數能幹神馬???

如果你把該函數在坐標軸上畫出來,sigmoid函數長這個樣子:

觀察上面這張圖,我們的問題迎刃而解:sigmoid函數能夠把一個實數,轉換成為[0-1]之間的數,也就是可以轉換成概率!!!

了解了這個函數的意義比求梯度的意義重要一萬倍!!!

下面求其梯度:

我們假設輸入x是一個實數,那麼對其求導得到 sigma(x) = frac{e^x}{{(1+e^{x})}^2} (這裡我們只關注結果,忽略求導過程,如果讀者讀者感興趣,可以自行計算),進而通過一些列計算,可以表示為 sigma(x) = sigma(x)(1-sigma(x))

2. 推導交叉熵函數關於softmax向量的梯度

在自然語言處理(NLP)基礎概念(一):詞向量及其生成方法介紹的4.2節中,我們提到過連續袋模型,其中,為了評估預測值與真實值的差距,我們引入了測量函數:交叉熵(cross entropy)。

交叉熵就是計算真實值 y ,和預測值 bar y 之間的『距離』。

其中y是one-hot向量。

那麼如果我們能夠得到交叉熵對於輸入矩陣 theta 的梯度,並使其最小,那麼本質上我們就是求得「什麼樣的 theta 能夠使交叉熵最小」。

很顯然,這對訓練神經網路模型意義重大。

這裡,我們表示交叉熵函數如下: H(bar{y},y) = - sum_{j=1}^{|V|}{y_jlog{bar{y_j}}}

那麼對其求導能夠得到(這裡省略了求導過程): H(bar{y},y) = bar y-y

這個結果說明,當預測值等於真實值時,梯度最小。

3. 推導包含一個隱藏層神經網路的梯度

推導包含一個隱藏層神經網路的梯度,也就是說得到 frac{partial J}{partial x} ,其中 J=CE(y,bar y) ,是這個神經網路的測量函數。這裡的x是輸入層的輸入。

而上面第二部分,我們是對 theta 求導,是輸出層的輸入。

為了說清楚,用下圖舉例說明:

首先,讓我們弄清楚一件事,x是輸入層的輸入,那麼從輸入層到輸出層,x經歷了什麼呢?

見以下分割線包含的部分。

====================================================================

首先經歷從輸入層到隱藏層:

  1. 先給x加權重和偏差: z_1=xW_1+b_1
  2. z_1 代入激活函數(這裡sigmoid是隱藏層的激活函數(activation function)得到h=sigmoid(xW_1+b_1)

然後,從隱藏層的輸出h到輸出層的輸出 bar y經歷了如下過程:

  1. 同樣增加權重和偏差: z_2=hW_2+b_2
  2. z_2 代入激活函數(這裡是softmax函數,見自然語言處理(NLP)基礎概念(二):Softmax介紹及其python實現)就得到了我們的預測值,也就是輸出層的輸出值 bar y

====================================================================

現在,我們要做的是,求測量函數對於x的梯度。而第二節中,我們求的是測量函數對於輸出層的輸入的梯度,也就是對於 z_2 的梯度。

現在我們開始推導:

  1. frac{partial{CE}}{partial x} 比較困難,而求 frac{partial z_1}{partial x} 比較簡單,所以我們利用鏈式法則(chain rule得到: frac{partial{CE}}{partial x}=frac{partial{CE}}{partial z_1}frac{partial{z_1}}{partial x}
  2. 但是這裡又遇到了同樣的問題,求 frac{partial{CE}}{partial z_1} 比較困難。我們可以繼續應用鏈式法則,這樣一直應用,最後得到: frac {partial CE}{x} = frac {partial CE}{z_2} frac {partial z_2}{h} frac {partial h}{z_1} frac {partial z_1}{x} 下面分別求這4個偏導數:
  3. 其中  frac {partial CE}{z_2} 我們已經在第二部分求過了,是 bar y-y
  4. 根據矩陣求導法則frac {partial z_2}{h} W_2^T
  5. 求 h相對於z_1的導數,這部分我們也已經得到了答案(見本文的第一部分),所以frac {partial h}{z_1}=sigma(z) = sigma(z)(1-sigma(z))
  6. 同樣根據矩陣求導法則, frac {partial z_1}{x} W_1^T

問題迎刃而解!

這裡,最重要的問題來了!!!求 frac{partial{CE}}{partial x} 的意義是什麼???

問題的答案也顯而易見,我們想知道,輸入值是什麼時,CE最小。

這也是測試函數CE的存在意義(回想一下測試函數的定義),輸入等於什麼的情況下,預測值 bar y 和真實值 y 之間的『距離』最小。

神經網路也是利用這一點優化模型參數,從而得到最好的結果。

4. 神經網路參數的討論

Dx代表輸入的維度, H代表隱藏層的神經元數量, Dy代表輸出的維度,那麼下圖的神經網路中參數數量是多少呢?

先給出答案:(Dx+1)*H+(H+1)*Dy

首先,這是一個只有一層隱藏層的神經網路。

輸入的維度是Dx的話,那麼輸入層的數量就是Dx+1,這裡為什麼+1,因為1乘以一個數就是一個實數,代表 z=Wx+b 中的b。

我們想知道的是參數的數量,也就是有多少W,有多少b。

很顯然,Dx是多少就有多少W,b只有一個,所以對於一個神經元來說參數數量是Dx+1。

現在有H個神經元,那麼參數數量就是(Dx+1)*H。

同樣的道理,我們可以得出,從隱藏層到輸出層有(H+1)*Dy個參數。

所以參數的總數量就是(Dx+1)*H+(H+1)*Dy。

這裡還可以再進一步推導,如果我們有N個隱藏層,每一層有(1~m)個神經元呢?讀者可以安裝上面的思路自行推導。

5. sigmoid函數及其梯度的python實現

下面是sigmoid函數以及求其梯度的python代碼,代碼包括三個函數。

其中sigmoid(x)用來對x進行sigmoid。

sigmoid_grad(x)用來計算sigmoid函數的梯度。這裡有個技巧,就是利用了文章第一部分的結論。

test_sigmoid_basic()函數提供了幾個例子用於測試前兩個函數。

腳本可以被直接執行:

if __name__ == "__main__":n

上面這行代碼的作用是「讓你寫的腳本模塊既可以導入到別的模塊中用,另外該模塊自己也可執行。」(「Make a script both importable and executable」)如果還是不太明白可以看這篇文章。

import numpy as npnndef sigmoid(x):n """n Compute the sigmoid function for the input here.nn Arguments:n x -- A scalar or numpy array.nn Return:n s -- sigmoid(x)n """n s = 1./(1.+np.exp(-x))n return snndef sigmoid_grad(s):n """n Compute the gradient for the sigmoid function here. Note thatn for this implementation, the input s should be the sigmoidn function value of your original input x.nn Arguments:n s -- A scalar or numpy array.nn Return:n ds -- Your computed gradient.n """nn ds = s*(1-s)nn return dsnndef test_sigmoid_basic():n """n Some simple tests to get you started.n Warning: these are not exhaustive.n """n print "Running basic tests..."n x = np.array([[1, 2], [-1, -2]])n f = sigmoid(x)n g = sigmoid_grad(f)n print fn f_ans = np.array([n [0.73105858, 0.88079708],n [0.26894142, 0.11920292]])n assert np.allclose(f, f_ans, rtol=1e-05, atol=1e-06)n print gn g_ans = np.array([n [0.19661193, 0.10499359],n [0.19661193, 0.10499359]])n assert np.allclose(g, g_ans, rtol=1e-05, atol=1e-06)n print "You should verify these results by hand!n"nnif __name__ == "__main__":n test_sigmoid_basic();n

6. 梯度檢查器的python實現

為了方便以後檢查梯度是否正確,利用導數的定義對梯度進行驗證,以下是python實現:

import numpy as npnimport randomnn# First implement a gradient checker by filling in the following functionsndef gradcheck_naive(f, x):n """ Gradient check for a function f.nn Arguments:n f -- a function that takes a single argument and outputs then cost and its gradientsn x -- the point (numpy array) to check the gradient atn """nn rndstate = random.getstate()n random.setstate(rndstate)n fx, grad = f(x) # Evaluate function value at original pointn h = 1e-4 # Do not change this!nn # Iterate over all indexes in xn # flags=[multi_index] can get all dimension index n # Set op_flags to make array readable and writable.n it = np.nditer(x, flags=[multi_index], op_flags=[readwrite])n while not it.finished:n ix = it.multi_indexnn # Try modifying x[ix] with h defined above to computen # numerical gradients. Make sure you call random.setstate(rndstate)n # before calling f(x) each time. This will make it possiblen # to test cost functions with built in randomness later.nn random.setstate(rndstate)n tmp1 = np.copy(x) n tmp1[ix] = tmp1[ix] + hn f1, _ = f(tmp1)n n random.setstate(rndstate)n tmp2 = np.copy(x) n tmp2[ix] = tmp2[ix] - hn f2, _ = f(tmp2)n numgrad = (f1 - f2) / (2 * h)nn print(numgrad,grad[ix])n print(max(1,abs(numgrad), abs(grad[ix])))nn # Compare gradientsn reldiff = abs(numgrad - grad[ix]) / max(1, abs(numgrad), abs(grad[ix]))n if reldiff > 1e-5:n print "Gradient check failed."n print "First gradient error found at index %s" % str(ix)n print "Your gradient: %f t Numerical gradient: %f" % (n grad[ix], numgrad)n returnnn it.iternext() # Step to next dimensionnn print "Gradient check passed!"nndef sanity_check():n """n Some basic sanity checks.n """n quad = lambda x: (np.sum(x ** 2), x * 2)nn print "Running sanity checks..."n gradcheck_naive(quad, np.array(123.456)) # scalar testn gradcheck_naive(quad, np.random.randn(3,)) # 1-D testn gradcheck_naive(quad, np.random.randn(4,5)) # 2-D testn print ""nnif __name__ == "__main__":n sanity_check()n

這裡包括兩個函數,gradcheck_naive(f, x)用於檢查梯度,def sanity_check()用於生成幾個例子進行測試。

gradcheck_naive(f, x)的主要思想是『用定義求出來的導數』和『用我們自己寫的函數求出來的導數』進行對比,如果結果不相等(代碼中是比對結果> 1e-5)就報錯。

這裡我們利用一個簡單的函數 f(x)=x^2 進行測試,我們知道它的導數是 f(x)=2x

這個測試函數的python實現為:quad = lambda x: (np.sum(x ** 2), x * 2)

如果不明白lambda的作用,看這裡。

在gradcheck_naive(f, x)函數中,我們首先定義一個極小的變數h = 1e-4

那麼根據導數的定義,f(x)=(f(x+h)-f(x-h))/2h

上述定義的python實現是下面這段代碼:

random.setstate(rndstate)n tmp1 = np.copy(x) n tmp1[ix] = tmp1[ix] + hn f1, _ = f(tmp1)n n random.setstate(rndstate)n tmp2 = np.copy(x) n tmp2[ix] = tmp2[ix] - hn f2, _ = f(tmp2)n numgrad = (f1 - f2) / (2 * h)nn print(numgrad,grad[ix])n print(max(1,abs(numgrad), abs(grad[ix])))n

最後我們比較定義和函數求出來的值是否一致,代碼如下:

reldiff = abs(numgrad - grad[ix]) / max(1, abs(numgrad), abs(grad[ix]))n

注意,這裡為什麼有個max(1, abs(numgrad), abs(grad[ix]))?

因為如果 abs(numgrad), abs(grad[ix])是小於1的數的話,除以一個小於一的數使結果變得更大,即便這兩個值真的很接近。

7. 神經網路的正向和反向傳遞及其python實現

#!/usr/bin/env pythonnnimport numpy as npnimport randomnnfrom q1_softmax import softmaxnfrom q2_sigmoid import sigmoid, sigmoid_gradnfrom q2_gradcheck import gradcheck_naivennndef forward_backward_prop(data, labels, params, dimensions):n """n Forward and backward propagation for a two-layer sigmoidal networknn Compute the forward propagation and for the cross entropy cost,n and backward propagation for the gradients for all parameters.nn Arguments:n data -- M x Dx matrix, where each row is a training example.n labels -- M x Dy matrix, where each row is a one-hot vector.n params -- Model parameters, these are unpacked for you.n dimensions -- A tuple of input dimension, number of hidden unitsn and output dimensionn """nn ### Unpack network parameters (do not modify)n ofs = 0n Dx, H, Dy = (dimensions[0], dimensions[1], dimensions[2])nn W1 = np.reshape(params[ofs:ofs+ Dx * H], (Dx, H))n ofs += Dx * Hn b1 = np.reshape(params[ofs:ofs + H], (1, H))n ofs += Hn W2 = np.reshape(params[ofs:ofs + H * Dy], (H, Dy))n ofs += H * Dyn b2 = np.reshape(params[ofs:ofs + Dy], (1, Dy))nn ### YOUR CODE HERE: forward propagationn h = sigmoid(np.dot(data,W1) +b1)n pred = sigmoid(np.dot(h, W2) + b2) n cost = (-1) * np.sum(labels * np.log(pred) + (1 - labels) * np.log(1 - pred))n ### END YOUR CODEnn ### YOUR CODE HERE: backward propagationn dout = pred - labels n dh = np.dot(dout, W2.T) * sigmoid_grad(h) n n gradW2 = np.dot(h.T, dout) n gradb2 = np.sum(dout, 0) n gradW1 = np.dot(data.T, dh)n gradb1 = np.sum(dh, 0)n ### END YOUR CODEnn ### Stack gradients (do not modify)n grad = np.concatenate((gradW1.flatten(), gradb1.flatten(),n gradW2.flatten(), gradb2.flatten()))n return cost, gradnndef sanity_check():n """n Set up fake data and parameters for the neural network, and test usingn gradcheck.n """n print "Running sanity check..."nn N = 20n dimensions = [10, 5, 10]n data = np.random.randn(N, dimensions[0]) # each row will be a datumnn # creat one-hot vectorn labels = np.zeros((N, dimensions[2]))n for i in xrange(N):n labels[i, random.randint(0,dimensions[2]-1)] = 1nn params = np.random.randn((dimensions[0] + 1) * dimensions[1] + (n dimensions[1] + 1) * dimensions[2], )nn gradcheck_naive(lambda params:n forward_backward_prop(data, labels, params, dimensions), params)nnif __name__ == "__main__":n sanity_check()n

推薦閱讀:

SPPNet-引入空間金字塔池化改進RCNN
擔心的事情還是發生了,AI水軍你根本看不出來
Python · RNN
為什麼梯度反方向是函數值局部下降最快的方向?

TAG:神经网络 | 机器学习 |