Lesson 6.动态计算图与梯度下降入门
? ? ? ?在《Lesson 5.基本優化思想與最小二乘法》的結尾,我們提到PyTorch中的AutoGrad(自動微分)模塊,并簡單嘗試使用該模塊中的autograd.grad進行函數的微分運算,我們發現,autograd.grad函數可以靈活進行函數某一點的導數或偏導數的運算,但微分計算其實也只是AutoGrad模塊中的一小部分功能。本節課,我們將繼續講解AutoGrad模塊中的其他常用功能,并在此基礎上介紹另一個常用優化算法:梯度下降算法。
import numpy as np import torch一、AutoGrad的回溯機制與動態計算圖
1.可微分性相關屬性
??在上一節中我們提到,新版PyTorch中的張量已經不僅僅是一個純計算的載體,張量本身也可支持微分運算。這種可微分性其實不僅體現在我們可以使用grad函數對其進行求導,更重要的是這種可微分性會體現在可微分張量參與的所有運算中。
- requires_grad屬性:可微分性
不難發現,z也同時存儲了張量計算數值、z是可微的,并且z還存儲了和y的計算關系(add)。據此我們可以知道,在PyTorch的張量計算過程中,如果我們設置初始張量是可微的,則在計算過程中,每一個由原張量計算得出的新張量都是可微的,并且還會保存此前一步的函數關系,這也就是所謂的回溯機制。而根據這個回溯機制,我們就能非常清楚掌握張量的每一步計算,并據此繪制張量計算圖。
2.張量計算圖
??借助回溯機制,我們就能將張量的復雜計算過程抽象為一張圖(Graph),例如此前我們定義的x、y、z三個張量,三者的計算關系就可以由下圖進行表示。
- 計算圖的定義
??上圖就是用于記錄可微分張量計算關系的張量計算圖,圖由節點和有向邊構成,其中節點表示張量,邊表示函數計算關系,方向則表示實際運算方向,張量計算圖本質是有向無環圖。
- 節點類型
??在張量計算圖中,雖然每個節點都表示可微分張量,但節點和節點之間卻略有不同。就像在前例中,y和z保存了函數計算關系,但x沒有,而在實際計算關系中,我們不難發現z是所有計算的終點,因此,雖然x、y、z都是節點,但每個節點卻并不一樣。此處我們可以將節點分為三類,分別是:
a):葉節點,也就是初始輸入的可微分張量,前例中x就是葉節點;
b):輸出節點,也就是最后計算得出的張量,前例中z就是輸出節點;
c):中間節點,在一張計算圖中,除了葉節點和輸出節點,其他都是中間節點,前例中y就是中間節點。
當然,在一張計算圖中,可以有多個葉節點和中間節點,但大多數情況下,只有一個輸出節點,若存在多個輸出結果,我們也往往會將其保存在一個張量中。
3.計算圖的動態性
??值得一提的是,PyTorch的計算圖是動態計算圖,會根據可微分張量的計算過程自動生成,并且伴隨著新張量或運算的加入不斷更新,這使得PyTorch的計算圖更加靈活高效,并且更加易于構建,相比于先構件圖后執行計算的部分框架(如老版本的TensorFlow),動態圖也更加適用于面向對象編程。
二、反向傳播與梯度計算
1.反向傳播的基本過程
??在《Lesson 5.》中,我們曾使用autograd.grad進行函數某一點的導數值得計算,其實,除了使用函數以外,我們還有另一種方法,也能進行導數運算:反向傳播。當然,此時導數運算結果我們也可以有另一種解讀:計算梯度結果。
注:此處我們暫時不區分微分運算結果、導數值、梯度值三者區別,目前位置三個概念相同,后續講解梯度下降時再進行區分。
首先,對于某一個可微分張量的導數值(梯度值),存儲在grad屬性中。
x.grad在最初,x.grad屬性是空值,不會返回任何結果,我們雖然已經構建了x、y、z三者之間的函數關系,x也有具體取值,但要計算x點導數,還需要進行具體的求導運算,也就是執行所謂的反向傳播。所謂反向傳播,我們可以簡單理解為,在此前記錄的函數關系基礎上,反向傳播函數關系,進而求得葉節點的導數值。在必要時求導,這也是節省計算資源和存儲空間的必要規定。
z #tensor(2., grad_fn=<AddBackward0>)z.grad_fn #<AddBackward0 at 0x7fad381971c0># 執行反向傳播 z.backward() '''反向傳播結束后,即可查看葉節點的導數值'''x #tensor(1., requires_grad=True)# 在z=y+1=x**2+1函數關系基礎上,x取值為1時的導數值 x.grad #tensor(2.)'''注意,在默認情況下,在一張計算圖上執行反向傳播,只能計算一次,再次調用backward方法將報錯''' z.backward() #--------------------------------------------------------------------------- #RuntimeError Traceback (most recent call last) #<ipython-input-52-40c0c9b0bbab> in <module> #----> 1 z.backward()當然,在y上也能執行反向傳播
x = torch.tensor(1.,requires_grad = True) y = x ** 2 z = y + 1y.backward()x.grad #tensor(2.)'''第二次執行時也會報錯''' y.backward() #--------------------------------------------------------------------------- #RuntimeError Traceback (most recent call last) #<ipython-input-60-ab75bb780f4c> in <module> #----> 1 y.backward() z.backward() #--------------------------------------------------------------------------- #RuntimeError Traceback (most recent call last) #<ipython-input-61-40c0c9b0bbab> in <module> #----> 1 z.backward()'''無論何時,我們只能計算葉節點的導數值''' y.grad #D:\Users\ASUS\anaconda3\lib\site-packages\ipykernel_launcher.py:1: UserWarning: #The .grad attribute of a Tensor that is not a leaf Tensor is being accessed. Its #.grad attribute won't be populated during autograd.backward(). If you indeed want #the gradient for a non-leaf Tensor, use .retain_grad() on the non-leaf Tensor. If #you access the non-leaf Tensor by mistake, make sure you access the leaf Tensor #instead. See github.com/pytorch/pytorch/pull/30531 for more informations. # """Entry point for launching an IPython kernel.至此,我們就了解了反向傳播的基本概念和使用方法:
- 反向傳播的本質:函數關系的反向傳播(不是反函數);
- 反向傳播的執行條件:擁有函數關系的可微分張量(計算圖中除了葉節點的其他節點);
- 反向傳播的函數作用:計算葉節點的導數/微分/梯度運算結果;
2.反向傳播運算注意事項
- 中間節點反向傳播和輸出節點反向傳播區別
??盡管中間節點也可進行反向傳播,但很多時候由于存在復合函數關系,中間節點反向傳播的計算結果和輸出節點反向傳播輸出結果并不相同。
x = torch.tensor(1.,requires_grad = True) y = x ** 2 z = y ** 2 z.backward() x.grad #tensor(4.)x = torch.tensor(1.,requires_grad = True) y = x ** 2 z = y ** 2 y.backward() x.grad #tensor(2.)- 中間節點的梯度保存
??默認情況下,在反向傳播過程中,中間節點并不會保存梯度
x = torch.tensor(1.,requires_grad = True) y = x ** 2 z = y ** 2 z.backward() y.grad #D:\Users\ASUS\anaconda3\lib\site-packages\ipykernel_launcher.py:2: UserWarning: #The .grad attribute of a Tensor that is not a leaf Tensor is being accessed. Its #.grad attribute won't be populated during autograd.backward(). If you indeed want #the gradient for a non-leaf Tensor, use .retain_grad() on the non-leaf Tensor. If #you access the non-leaf Tensor by mistake, make sure you access the leaf Tensor #instead. See github.com/pytorch/pytorch/pull/30531 for more informations. x.grad #tensor(4.)'''若想保存中間節點的梯度,我們可以使用retain_grad()方法''' x = torch.tensor(1.,requires_grad = True) y = x ** 2 y.retain_grad() z = y ** 2 z.backward() y #tensor(1., grad_fn=<PowBackward0>) y.grad #tensor(2.) x.grad #tensor(4.)3.阻止計算圖追蹤
??在默認情況下,只要初始張量是可微分張量,系統就會自動追蹤其相關運算,并保存在計算圖關系中,我們也可通過grad_fn來查看記錄的函數關系,但在特殊的情況下,我們并不希望可微張量從創建到運算結果輸出都被記錄,此時就可以使用一些方法來阻止部分運算被記錄。
- with torch.no_grad():阻止計算圖記錄
??例如,我們希望x、y的函數關系被記錄,而y的后續其他運算不被記錄,可以使用with torch.no_grad()來組織部分y的運算不被記錄。
x = torch.tensor(1.,requires_grad = True) y = x ** 2with torch.no_grad():z = y ** 2'''with相當于是一個上下文管理器,with torch.no_grad()內部代碼都“屏蔽”了計算圖的追蹤記錄''' z #tensor(1.)z.requires_grad #Falsey #tensor(1., grad_fn=<PowBackward0>)- .detach()方法:創建一個不可導的相同張量
在某些情況下,我們也可以創建一個不可導的相同張量參與后續運算,從而阻斷計算圖的追蹤
x = torch.tensor(1.,requires_grad = True) y = x ** 2 y1 = y.detach() z = y1 ** 2y #tensor(1., grad_fn=<PowBackward0>)y1 #tensor(1.)z #tensor(1.)4.識別葉節點
??由于葉節點較為特殊,如果需要識別在一個計算圖中某張量是否是葉節點,可以使用is_leaf屬性查看對應張量是否是葉節點。
x.is_leaf #Truey.is_leaf #False'''但is_leaf方法也有容易混淆的地方,對于任何一個新創建的張量,無論是否可導、是否加入計算圖,都是可以是葉節點,這些節點距離真正的葉節點,只差一個requires_grad屬性調整。''' torch.tensor([1]).is_leaf #True# 經過detach的張量,也可以是葉節點 y1 #tensor(1.)y1.is_leaf #True三、梯度下降基本思想
??有了AutoGrad模塊中各函數方法的支持,接下來,我們就能嘗試手動構建另一個優化算法:梯度下降算法。
1.最小二乘法的局限與優化
??在《Lesson 5.》中,我們嘗試使用最小二乘法求解簡單線性回歸的目標函數,并順利的求得了全域最優解。但正如上節所說,在所有的優化算法中最小二乘法雖然高效并且結果精確,但也有不完美的地方,核心就在于最小二乘法的使用條件較為苛刻,要求特征張量的交叉乘積結果必須是滿秩矩陣,才能進行求解。而在實際情況中,很多數據的特征張量并不能滿足條件,此時就無法使用最小二乘法進行求解。
最小二乘法結果:
??當最小二乘法失效的情況時,其實往往也就代表原目標函數沒有最優解或最優解不唯一。針對這樣的情況,有很多中解決方案,例如,我們可以在原矩陣方程中加入一個擾動項𝜆𝐼,修改后表達式如下:
其中,𝜆是擾動項系數,𝐼是單元矩陣。由矩陣性質可知,加入單位矩陣后,(𝑋^𝑇𝑋+𝜆𝐼)部分一定可逆,而后即可直接求解𝑤?^𝑇?,這也就是嶺回歸的一般做法。
??當然,上式修改后求得的結果就不再是全域最小值,而是一個接近最小值的點。鑒于許多目標函數本身也并不存在最小值或者唯一最小值,在優化的過程中略有偏差也是可以接受的。當然,伴隨著深度學習的逐漸深入,我們會發現,最小值并不唯一存在才是目標函數的常態。基于此情況,很多根據等式形變得到的精確的求解析解的優化方法(如最小二乘)就無法適用,此時我們需要尋找一種更加通用的,能夠高效、快速逼近目標函數優化目標的最優化方法。在機器學習領域,最通用的求解目標函數的最優化方法就是著名的梯度下降算法。
??值得一提的是,我們通常指的梯度下降算法,并不是某一個算法,而是某一類依照梯度下降基本理論基礎展開的算法簇,包括梯度下降算法、隨機梯度下降算法、小批量梯度下降算法等等。接下來,我們就從最簡單的梯度下降入手,講解梯度下降的核心思想和一般使用方法。
2.梯度下降核心思想
??梯度下降的基本思想其實并不復雜,其核心就是希望能夠通過數學意義上的迭代運算,從一個隨機點出發,一步步逼近最優解。
例如,在此前求解簡單線性回歸方程的過程中,我們曾查看SSE的三維函數圖像如下:
from matplotlib import pyplot as plt from mpl_toolkits.mplot3d import Axes3Dx = np.arange(-1,3,0.05) y = np.arange(-1,3,0.05) a, b = np.meshgrid(x, y) SSE = (2 - a - b) ** 2 + (4 - 3 * a - b) ** 2fig = plt.figure() ax = plt.axes(projection='3d')ax.plot_surface(a, b, SSE, cmap='rainbow') ax.contour(a, b, SSE, zdir='z', offset=0, cmap="rainbow") #生成z方向投影,投到x-y平面 plt.show()而梯度下降,作為最優化算法,核心目標也是找到或者逼近最小值點,而其基本過程則:
- 在目標函數上隨機找到一個初始點;
- 通過迭代運算,一步步逼近最小值點;
數學意義上的迭代運算,指的是上一次計算的結果作為下一次運算的初始條件帶入運算
3.梯度下降的方向與步長
??當然,梯度下降的基本思想好理解,但實現起來卻并不容易(這也是大多數機器學習算法的常態)。在實際沿著目標函數下降的過程中,我們核心需要解決兩個問題,其一是往哪個方向走,其二是每一步走多遠。以上述簡單線性回歸的目標函數為例,在三維空間中,目標函數上的每個點理論上都有無數個移動的方向,每次移動多遠的物理距離也沒有明顯的約束,而這些就是梯度下降算法核心需要解決的問題,也就是所謂的方向和步長。
首先,是關于方向的討論。
關于方向的討論,其實梯度下降是采用了一種局部最優推導全域最優的思路,我們首先是希望能夠找到讓目標函數變化最快的方向作為移動的方向,而這個方向,就是梯度。
3.1 導數與梯度
??我們都知道,函數上某一點的導數值的幾何含義就是函數在該點上切線的斜率。例如y=x**2中,x在1點的導數就是函數在1點的切線的斜率。
x = np.arange(-10,10,0.1) y = x ** 2 # y = 2x z = 2 * x - 1 # 在(1,1)點的切線方程 plt.plot(x, y, '-') plt.plot(x, z, 'r-') plt.plot(1, 1, 'bo') plt.show()?
而更進一步來講,對于上述函數,x取值為1的時候,導數和切線的斜率為2,代表含義是給1這個點一個無窮小的增量,1只能沿著切向方向移動(但仍然在曲線上)。當然,該點導數值的另外一個解釋就是該點的梯度,梯度的值(grad)和導數相同,而梯度的概念可以視為導數概念的延申,只不過梯度更側重方向的概念,也就是從梯度的角度解讀導數值,就代表著當前這個點的可以使得y值增加最快的移動方向。
梯度:梯度本身是一個代表方向的矢量,代表某一函數在該點處沿著梯度方向變化時,變化率最大。當然,梯度的正方向代表函數值增長最快的方向,梯度的負方向表示函數減少最快的方向。
x = torch.tensor(1., requires_grad = True) y = x ** 2 y.backward() x.grad #tensor(2.)不過此時由于自變量存在一維空間,只能沿著x軸變化(左右移動,只有兩個方向),梯度給出的方向只能解讀為朝著2,也就是正方向變化時,y的增加最快(確實如此,同時也顯而易見)。
3.2 梯度與方向
??為了更好的解讀梯度與方向之間的關系,我們以《Lesson 5.》中簡單線性回歸損失函數為例來進行查看。我們有目標函數及其圖像如下:
fig = plt.figure() ax = plt.axes(projection='3d')ax.plot_surface(a, b, SSE, cmap='rainbow') ax.contour(a, b, SSE, zdir='z', offset=0, cmap="rainbow") #生成z方向投影,投到x-y平面 plt.show()此時a、b是在實數域上取值。假設二者初始值為0,也就是初始隨機點為原點。對于(0,0)點,有梯度計算如下
a = torch.tensor(0., requires_grad = True) a #tensor(0., requires_grad=True)b = torch.tensor(0., requires_grad = True) b #tensor(0., requires_grad=True)s0 = torch.pow((2 - a - b), 2) + torch.pow((4 - 3 * a - b), 2) s0 #tensor(20., grad_fn=<AddBackward0>) s0.backward() a.grad, b.grad #(tensor(-28.), tensor(-12.)) '''也就是原點和(-28,-12)這個點之間連成直線的方向,就是能夠使得sse變化最快的方向,并且朝向(-28,-12)方向就是使得sse增加最快的方向,反方向則是令sse減少最快的方向。''' # 通過繪制直線,確定原點的移動方向 x = np.arange(-30,30,0.1) y = (12/28) * x plt.plot(x, y, '-') plt.plot(0, 0, 'ro') plt.plot(-28, -12, 'ro')Point:這里有關于方向的兩點討論
- 方向沒有大小,雖然這是個顯而易見的觀點,但我們當我們說朝著(-28,-12)方向移動,只是說沿著直線移動,并非一步移動到(-28,-12)上;
- 方向跟隨梯度,隨時在發生變化。值得注意的是,一旦點發生移動,梯度就會隨之發生變化,也就是說,哪怕是沿著讓sse變化最快的方向移動,一旦“沿著方向”移動了一小步,這個方向就不再是最優方向了。
當然,逆梯度值的方向變化是使得sse變小的最快方向,我們嘗試移動“一小步”。一步移動到(28,12)是沒有意義的,梯度各分量數值的絕對值本身也沒有距離這個層面的數學含義。由于a和b的取值要按照(28,12)等比例變化,因此我們不妨采用如下方法進行移動:
s0 #tensor(20., grad_fn=<AddBackward0>)a = torch.tensor(0.28, requires_grad = True) a #tensor(0.2800, requires_grad=True) b = torch.tensor(0.12, requires_grad = True) b #tensor(0.1200, requires_grad=True) s1 = (2 - a - b) ** 2 + (4 - 3 * a - b) ** 2 s1 #tensor(11.8016, grad_fn=<AddBackward0>)'''確實有所下降,繼續求解新的點的梯度''' s1.backward() a.grad, b.grad #(tensor(-21.4400), tensor(-9.2800))不難看出,方向已經發生變化。其實無論移動“多小”一步,只要移動,方向就需要重新計算。如果每個點的梯度提供了移動方向的最優解,那移動多長,其實并沒有統一的規定。這里,我們將上述0.01稱作學習率,而學習率乘以梯度,則是原點移動的“長度”。
當然,在移動到(0.28,0.12)之后,還沒有取到全域最優解,因此還需要繼續移動,當然我們還可以繼續按照0.01這個學習率繼續移動,此時,新的梯度為(-21.44,-9.28),則有
接下來,我們可以繼續計算新的(0.94,0.148)這個點的梯度,然后繼續按照學習率0.01繼續移動,在移動若干次之后,就將得到非常接近于(1,1)的結果。
四、梯度下降的數學表示
1.梯度下降的代數表示
??根據上述描述過程,我們可以通過代數運算方式總結梯度下降運算的一般過程
令多元線性回歸方程為
令
出于加快迭代收斂速度的目標,我們在定義梯度下降的損失函數L時,在原SSE基礎上進行比例修正,新的損失函數𝐿(𝑤1,𝑤2,...,𝑤𝑑,𝑏)=1/(2m)*SSE,其中,m為樣本個數。?
損失函數有:
并且,根據此前描述過程,在開始梯度下降求解參數之前,我們首先需要設置一組參數的初始取值(𝑤1,𝑤2...,𝑤𝑑,𝑏),以及學習率𝛼,然后即可執行迭代運算,其中每一輪迭代過程需要執行以下三步
Step 1.計算梯度表達式
?Step 2.用學習率乘以損失函數梯度,得到迭代移動距離
?Step 3.用原參數減Step 2中計算得到的距離,更新所有的參數w
更新完所有參數,即完成了一輪的迭代,接下來就能以新的一組𝑤𝑖參與下一輪迭代。
上一輪計算結果作為下一輪計算的初始值,就是所謂的迭代。
而何時停止迭代,一般來說有兩種情況,其一是設置迭代次數,到達迭代次數即停止迭代;其二則是設置收斂區間,即當某兩次迭代過程中,每個𝑤𝑖更新的數值都小于某個預設的值,則停止迭代。
2.再次理解步長
根據梯度下降的線性代數表示方法,我們可以通過某個實例來強化理解步長這一概念。
有數據集表示如下:
?假設,我們使用𝑦=𝑤𝑥進行擬合,則SSE為:
此時,SSE就是一個關于w的一元函數。當使用最小二乘法進行求解時,SSE就是損失函數,并且SSE對于w求導為0的點就是最小值點,因此有:
?但我們使用梯度下降求解時:
由于梯度表示方向,在某些情況下我們可以對其絕對數值進行一定程度上的“縮放”,此時我們規定有效梯度是原梯度的1/28,則有
設步長α=0.5,初始值點取為𝑤0=0,則迭代過程如下:
第一輪迭代:
第二輪迭代:
第三輪迭代:
第四輪迭代:
依次類推:
我們不難發現,如果損失函數是凸函數,并且全域最小值存在,則步長可以表示當前點和最小值點之間距離的比例關系。但總的來說,對于步長的設置,我們有如下初步結論:
- 步長太短:會極大的影響迭代收斂的時間,整體計算效率會非常低;
- 步長太長:容易跳過最優解,導致結果震蕩。
關于步長的設置,其實更多的會和實際使用情況相關,和實際損失函數特性相關,因此我們會在后續使用梯度下降求解目標函數時根據實際情況,講解步長的實際調整策略。
3.梯度下降的矩陣表示
??和最小二乘法一樣,代數表示形式易于理解但不易與代碼操作,在實際編程實現梯度下降的過程中,我們還是更傾向于使用矩陣來表示梯度下降計算過程。
令?
- 𝑤? :方程系數所組成的向量,并且我們將自變量系數和截距放到了一個向量中,此處𝑤? 就相當于前例中的a、b組成的向量(a,b);
- 𝑥? :方程自變量和1共同組成的向量;
因此,方程可表示為
另外,我們將所有自變量的值放在一個矩陣中,并且和此前A矩陣類似,為了捕捉截距,添加一列全為1的列在矩陣的末尾,設總共有m組取值,則
對應到前例中的A矩陣,A矩陣就是擁有一個自變量、兩個取值的X矩陣。令y為自變量的取值,則有
?此時,SSE可表示為:
梯度下降損失函數為:
同樣,我們需要設置初始化參數(𝑤1,𝑤2...,𝑤𝑑,𝑏),以及學習率𝛼,然后即可開始執行迭代過程,同樣,每一輪迭代需要有三步計算:
Step 1.計算梯度表達式
對于參數向量𝑤? ,其梯度計算表達式如下:
Step 2.用學習率乘以損失函數梯度,得到迭代移動距離
Step 3.用原參數減Step 2中計算得到的距離,更新所有的參數w
更新完所有參數,即完成了一輪的迭代,接下來就能以新的𝑤? 參與下一輪迭代。
五、手動實現梯度下降
??接下來,我們使用上述矩陣表示的梯度下降公式,圍繞此前的簡單線性回歸的目標函數,利用此前介紹的AutoGrad模塊中的梯度計算功能,來進行手動求解梯度下降。
在轉化為矩陣表示的過程中,我們令?
- 手動嘗試實現一輪迭代
總結
以上是生活随笔為你收集整理的Lesson 6.动态计算图与梯度下降入门的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Lesson 5.基本优化思想与最小二乘
- 下一篇: Lesson 7(12)神经网络的诞生与