-
Notifications
You must be signed in to change notification settings - Fork 16
grad
ChatGPT 背後是 GPT3/GPT4 模型,GPT 是基於 Google 所提出的 Attention 機制的一種深度學習(Deep Learning) 模型,深度學習說穿了其實就是原本的《神經網路》(Neural Network) ,不過由於加上了一些新的模型 (像是捲積神經網路 CNN, 循環神經網路 RNN,生成對抗網路 GAN 和目前在語言上表現最好的 Transformer 模型),以及在神經網路的層數上加深很多,從以往的 3-4 層,提升到了十幾層,甚至上百層,於是我們給這些新一代的《神經網路》技術一個統稱,那就是《深度學習》。
雖然《深度學習》的神經網路層數變多了,《網路模型》也多了一些,但是背後的學習算法和運作原理並沒有多大改變,仍然是以《梯度下降》(Gradient Descendent) 和《反傳遞算法》(Back Propagation) 為主。
但是《梯度下降》和《反傳遞算法》兩者,幾乎都是以數學的形式呈現,其中《梯度》的數學定義如下:
若把《梯度》當成一個《巨型算子》可以寫為如下形式:
這樣的數學雖然只是《基本的偏微分》,但是卻足以嚇倒很多人,包括我在內!
《反傳遞算法》的運作原理,則是建築在《微積分的鏈鎖規則》上,如以下算式所示:
根據這兩個數學式,人工智慧領域發展出了一整套《神經網路訓練算法》,稱為《反傳遞算法》,可以用來訓練《神經網路》,讓程式可以具有《函數優化》的能力。
有了函數優化的能力,程式就能向《一群樣本與解答》學習,優化《解答的能力》,進而解決《手寫辨識、語音辨識、影像辨識》甚至是《機器翻譯》等問題。
如果我們有個函數能計算《錯誤率》,那麼透過《優化算法》,我們就能找到讓錯誤率很低的函數,這個錯誤率很低的函數,就很少會在《訓練學習樣本》上回答錯誤。
如果這個函數還具有《通用延展性》,也就是在《非學習樣本上》也表現得同樣良好,那麼這個函數基本上就解決了該問題。
在本文中,我們將從《梯度下降法》開始,讓熟悉程式的人能夠輕易的透過《程式》來理解《深度學習背後的那些數學》!
梯度的基礎是偏微分,其實就是多變數函數對某一變數的微分,但是大學時期,我們常常都只學單變數微分,所以先讓我們回顧一下單變數微分的基本定義:
這個定義在視覺上就是斜率。(上述公式中的 h 在圖中用

如果把上列定義寫為程式,那就會像下列的 diff 函數這樣 (只是我們沒辦法對 h 取無限小,所以就取一個很小的步伐值,在此設 h 為 0.001)
h = 0.001
def diff(f, x):
df = f(x+h)-f(x)
return df/h
def square(x):
return x**2
def power3(x):
return x**3
print('diff(square,2)=', diff(square, 2)) # x**2 的微分為 2x, x=2 時斜率為 2*2 = 4
print('diff(power3,2)=', diff(power3, 2)) # x**3 的微分為 3x**2, x=2 時斜率為 3*2*2 = 12
'''
執行結果
$ python diff.py
diff(square,2)= 4.000999999999699
diff(power3,2)= 12.006000999997823
'''如前所述,《梯度》的數學定義如下:
問題是、這樣的數學符號對程式人有點可怕,到底梯度有甚麼直覺意義呢?讓我們看看下圖:

其實梯度就是斜率最大的那個方向,所以梯度下降法,其實就是朝著斜率最大的方向走。
如果我們朝著《斜率最大》方向的《正梯度》走,那麼就會愈走愈高,但是如果朝著《逆梯度》方向走,那麼就會愈走愈低。
而梯度下降法,就是朝著《逆梯度》的方向走,於是就可以不斷下降,直到到達梯度為 0 的點 (斜率最大的方向仍然是斜率為零),此時就已經到了一個《谷底》,也就是區域的最低點了!
理解了這個直覺概念之後,我們的下一個問題是,如何用程式來計算《梯度》呢?
其實、很多數學只要回到基本定義,就一點都不可怕了!
讓我們先回頭看看梯度中的基本元素,也就是偏微分,其定義是:
舉例而言,假如對
而對 y 的偏微分就是:
在以下程式中,於是我們寫了一個函數 df 來計算偏微分
例如
- df(f, p, 0) 是 f 對 x 的偏微分
- df(f, p, 1) 是 f 對 y 的偏微分
h = 0.01
# df(f, p, k) 為函數 f 對變數 k 的偏微分: df / dp[k]
# 例如在 p 是二維點的情況下,函數 f(p) = f(x,y) 的情況
# k=0 時偏微分是 df/dx, k=1 時偏微分是 df/dy
def df(f, p, k):
p1 = p.copy()
p1[k] += h
return (f(p1) - f(p)) / h
# 函數 f 在點 p 上的梯度
def grad(f, p):
gp = p.copy()
for k in range(len(p)):
gp[k] = df(f, p, k)
return gp
# f(x,y)=x*x+y*y
def f(p):
[x,y] = p
return x * x + y * y
p = [x,y] = [1,3]
print('p=', p) # p = [x,y] = [1, 3], 我們想算這一點的梯度
print('df(f, 0) = ', df(f, p, 0)) # x 方向的偏微分 df/dx = 2
print('df(f, 1) = ', df(f, p, 1)) # y 方向的偏微分 df/dy = 6
print('grad(f)=', grad(f, p)) # 梯度 = (df/dx, df/dy) = (2,6)
'''
執行結果
$ python grad.py
p= [1, 3]
df(f, 0) = 2.009999999999934
df(f, 1) = 6.009999999999849
grad(f)= [2.009999999999934, 6.009999999999849]
'''只要我們對每個變數都取偏微分,像程式中的 grad(f, p) 函數那樣,就能計算出《梯度》向量了!
在上述程式中,我們定義函數 f 為
但由於我們使用的是《浮點數》,而且 h 設為 0.001 ,所以計算出來的梯度有小小的誤差,那是正常的!
只要能計算梯度,那麼要實作《梯度下降法》就很容易了,我們可以呼叫上述的梯度函數 grad(f, p) ,輕而易舉地設計出《梯度下降法》程式如下:
# 使用梯度下降法尋找函數最低點
def gradientDescendent(f, p0, step=0.009, max_loops=100000, dump_period=1000):
p = np.array(p0)
for i in range(max_loops):
fp = f(p) # 目前的函數值
gp = grad(f, p) # 計算梯度
glen = norm(gp) # 梯度的長度 (步伐大小)
if i%dump_period == 0: # 每執行 dump_period 次才會印出一次結果
print('{:05d}:f(p)={:.3f} p={:s} gp={:s} glen={:.5f}'.format(i, fp, str(p), str(gp), glen))
if glen < 0.00001: # or fp0 < fp: # 如果步伐已經很小了,或者 f(p) 變大了,那麼就停止吧!
break
p += np.multiply(gp, -1*step) # 朝逆梯度方向的一小步
print('{:05d}:f(p)={:.3f} p={:s} gp={:s} glen={:.5f}'.format(i, fp, str(p), str(gp), glen))
return p # 傳回最低點!然後讓我們測試看看,該算法是否能找到
$ python gdArray.py
00000:f(p)=14.000 p=[0.00000 0.00000 0.00000] gp=[-1.99900 -3.99900 -5.99900] glen=7.48171
00050:f(p)=2.278 p=[0.59645 1.19320 1.78995] gp=[-0.80610 -1.61260 -2.41910] glen=3.01700
00100:f(p)=0.371 p=[0.83697 1.67436 2.51175] gp=[-0.32506 -0.65028 -0.97550] glen=1.21661
00150:f(p)=0.061 p=[0.93396 1.86839 2.80281] gp=[-0.13108 -0.26223 -0.39337] glen=0.49060
00200:f(p)=0.010 p=[0.97307 1.94663 2.92019] gp=[-0.05286 -0.10574 -0.15863] glen=0.19783
00250:f(p)=0.002 p=[0.98884 1.97818 2.96752] gp=[-0.02131 -0.04264 -0.06397] glen=0.07978
00300:f(p)=0.000 p=[0.99520 1.99090 2.98660] gp=[-0.00860 -0.01719 -0.02579] glen=0.03217
00350:f(p)=0.000 p=[0.99777 1.99603 2.99430] gp=[-0.00347 -0.00693 -0.01040] glen=0.01297
00400:f(p)=0.000 p=[0.99880 1.99810 2.99740] gp=[-0.00140 -0.00280 -0.00419] glen=0.00523
00450:f(p)=0.000 p=[0.99922 1.99894 2.99865] gp=[-0.00056 -0.00113 -0.00169] glen=0.00211
00500:f(p)=0.000 p=[0.99939 1.99927 2.99916] gp=[-0.00023 -0.00045 -0.00068] glen=0.00085
00550:f(p)=0.000 p=[0.99945 1.99941 2.99936] gp=[-0.00009 -0.00018 -0.00028] glen=0.00034
00600:f(p)=0.000 p=[0.99948 1.99946 2.99944] gp=[-0.00004 -0.00007 -0.00011] glen=0.00014
00650:f(p)=0.000 p=[0.99949 1.99949 2.99948] gp=[-0.00001 -0.00003 -0.00004] glen=0.00006
00700:f(p)=0.000 p=[0.99950 1.99949 2.99949] gp=[-0.00001 -0.00001 -0.00002] glen=0.00002
00745:f(p)=0.000 p=[0.99950 1.99950 2.99950] gp=[-0.00000 -0.00001 -0.00001] glen=0.00001
結果果然找到了 (x,y,z)=(0.99950 1.99950 2.99950),非常接近標準答案 (1,2,3) 的解,因此上述的方法,基本上就已經實作出《梯度下降法》了。
我們將完整的程式專案,放在以下 NumGrad 專案當中,請執行該專案並詳細閱讀之,這樣才能真正理解梯度下降法是如何運作的,這是《神經網路深度學習的最核心觀念》。
以上的《梯度下降法》,是採用計算
假如函數 f 的參數有 n 個,那麼要算出梯度,就必須重複的呼叫 n 次以上的 f 函數,因為每個變數都要計算偏微分,每次計算偏微分都要多算一次 f 函數。
換句話說,我們必須計算出每個
- 要算偏微分
$\frac{\partial f}{\partial x_1}$ ,就要先算出$f(x_1+h, x_2,...., x_n)$ - 要算偏微分
$\frac{\partial f}{\partial x_2}$ ,就要先算出$f(x_1, x_2+h, ...., x_n)$ - ...
- 要算偏微分
$\frac{\partial f}{\partial x_n}$ ,就要先算出$f(x_1, x_2, ...., x_n+h)$
這樣當參數數量 n 很大的時候,梯度的計算就會變得很慢,因此我們必須想辦法加速《梯度的計算速度》。
而《反傳遞演算法》,就是用來加速《梯度計算》的一種方法,這種方法依靠的是《自動微分》功能,想辦法從後面一層的差值,計算出前面一層應該調整的方向與大小。
我們將在下一章當中介紹《反傳遞演算法》。